Issue #3290929: Provide a farmOS map form element
This commit is contained in:
commit
62e54951f2
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- [Issue #3290929: Provide a farmOS map form element](https://www.drupal.org/project/farm/issues/3290929)
|
||||
|
||||
### Security
|
||||
|
||||
- Update Drupal core to 9.3.16 for [SA-CORE-2022-011](https://www.drupal.org/sa-core-2022-011).
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
# Maps
|
||||
|
||||
farmOS includes features for rendering and manipulating geometry data in
|
||||
map-based UIs.
|
||||
|
||||
It uses [farmOS-map](https://github.com/farmOS/farmOS-map), which is based on
|
||||
the open-source [OpenLayers](https://openlayers.org/) project. This includes
|
||||
tools for drawing and editing geometries, adding imagery and vector layers, and
|
||||
a framework for writing custom behaviors.
|
||||
|
||||
farmOS-map is maintained by the farmOS community as a standalone library for
|
||||
common agricultural mapping needs. It is designed to be reusable in any
|
||||
application with similar needs. It is not specific to or dependent on farmOS
|
||||
itself. Rather, farmOS includes it as a dependency, and provides some helpful
|
||||
wrappers for using it inside modules. This page describes how to use farmOS-map
|
||||
in farmOS modules.
|
||||
|
||||
For more information about the farmOS-map library itself and what it provides,
|
||||
refer to the farmOS-map documentation on GitHub:
|
||||
|
||||
[github.com/farmOS/farmOS-map](https://github.com/farmOS/farmOS-map)
|
||||
|
||||
## Render element
|
||||
|
||||
Maps can be embedded in pages as a `farm_map` type render element.
|
||||
|
||||
```php
|
||||
$build['mymap'] = [
|
||||
'#type' => 'farm_map',
|
||||
'#map_type' => 'default',
|
||||
'#map_settings' => [
|
||||
'mysetting' => 'myvalue',
|
||||
],
|
||||
'#behaviors' => [
|
||||
'mybehavior',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `#map_type` (optional) - See [Map types](#map-types). Defaults to `default`.
|
||||
- `#map_settings` (optional) - An array of map settings, which will be passed
|
||||
into the map instance's client-side JavaScript object, so they are available
|
||||
in behavior JavaScript.
|
||||
- `#behaviors` (optional) - See [Behaviors](#behaviors). Defaults to `[]` (but
|
||||
behaviors may also be added by map types and render events).
|
||||
|
||||
## Form element
|
||||
|
||||
Editable maps can be embedded in forms with a `farm_map_input` type element.
|
||||
These maps will have the drawing/editing controls enabled, allowing geometries
|
||||
to be added/edited/deleted directly in the map. A default value can be used to
|
||||
pre-populate the map with a geometry. A text field can be optionally displayed
|
||||
beneath the map to show the raw geometry data (auto-updates during editing).
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
$form['mymap'] = [
|
||||
'#type' => 'farm_map_input',
|
||||
'#title' => t('My Geometry'),
|
||||
'#map_type' => 'default',
|
||||
'#map_settings' => [
|
||||
'mysetting' => 'myvalue',
|
||||
],
|
||||
'#behaviors' => [
|
||||
'mybehavior',
|
||||
],
|
||||
'#display_raw_geometry' => TRUE,
|
||||
'#default_value' => 'POINT(-45.967095060886315 32.77503850904169)',
|
||||
];
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `#map_type` (same as render element, above)
|
||||
- `#map_settings` (same as render element, above)
|
||||
- `#behaviors` (same as render element, above)
|
||||
- `#display_raw_geometry` (optional) - Whether to show a text field below the
|
||||
map with the raw geometry value in Well-Known Text (WKT) format. Defaults to
|
||||
`FALSE`.
|
||||
- `#default_value` (optional) - The default geometry value to display in the
|
||||
map initially, in Well-Known Text (WKT) format. This geometry will be
|
||||
editable in the map unless `#disabled` is `TRUE`.
|
||||
|
||||
## Map types
|
||||
|
||||
farmOS modules can optionally define "map types", which are then referenced in
|
||||
the `#map_type` property of the render and form elements.
|
||||
|
||||
**This is optional and in most cases the `default` map type is sufficient.**
|
||||
|
||||
Map types are used to define reusable map configurations with common
|
||||
[behaviors](#behaviors). They can be targeted by [render event](#render-events)
|
||||
subscribers to add/modify behavior in certain contexts.
|
||||
|
||||
Map types are represented as Drupal config entities, installed via modules,
|
||||
just like asset types, log types, flags, etc.
|
||||
|
||||
A very simple example of a custom map type definition looks like this:
|
||||
|
||||
`my_module/config/install/farm_map.map_type.mymaptype.yml`
|
||||
|
||||
```yaml
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- my_module
|
||||
id: mymaptype
|
||||
label: My Map Type
|
||||
description: "My module's custom map type."
|
||||
behaviors: { }
|
||||
options: { }
|
||||
```
|
||||
|
||||
**Properties**
|
||||
|
||||
- `id` - A unique ID for the map type. This will be referenced in `#map_type`.
|
||||
- `label` - A human-readable label for the map type.
|
||||
- `description` - A human-readable description for the map type.
|
||||
- `behaviors` - A list of [behaviors](#behaviors) to attach to maps of this
|
||||
type by default.
|
||||
- `options` - Default options that will be merged with `#map_settings` and
|
||||
passed into `farmOS.map.create()`. See:
|
||||
[github.com/farmOS/farmOS-map#creating-a-map](https://github.com/farmOS/farmOS-map#creating-a-map)
|
||||
|
||||
## Behaviors
|
||||
|
||||
The farmOS-map library uses the concept of "behaviors" to encapsulate common
|
||||
and reusable sets of map behavior logic into JavaScript objects that can be
|
||||
"attached" to map instances.
|
||||
|
||||
Behaviors can be used to add layers to a map, add new buttons/controls, enable
|
||||
OpenLayers interactions, connect maps with other elements of a page like forms,
|
||||
etc.
|
||||
|
||||
For general information about farmOS-map behaviors, see:
|
||||
[github.com/farmOS/farmOS-map#adding-behaviors](https://github.com/farmOS/farmOS-map#adding-behaviors)
|
||||
|
||||
Some behaviors that farmOS provides include:
|
||||
|
||||
- `wkt` - Adds a vector layer to the map based on a Well-Known Text (WKT)
|
||||
string. Edit controls can be optionally enabled to allow drawing, modifying,
|
||||
moving, and deleting geometries within the map. This behavior is enabled
|
||||
automatically in the `farm_map_input` form element, and when `wkt` is
|
||||
included in `#map_settings.`
|
||||
- `input` - Listens for changes to geometries in the map and copies them to a
|
||||
form input (`textfield` or `hidden`) to be saved/manipulated server-side.
|
||||
This behavior is enabled automatically in the `farm_map_input` form element.
|
||||
- `popup` - Adds a popup interaction to the map, which appears when a geometry
|
||||
feature is clicked.
|
||||
- `asset_type_layers` - Adds asset geometry vector and cluster layers to a
|
||||
map. This behavior is responsible for adding the "Locations" layers on the
|
||||
farmOS dashboard map, the "Assets" and "Asset counts" layers to asset maps,
|
||||
automatically zooming to visible geometries, and adding asset details to
|
||||
popups when a geometry is clicked (depends on the `popup` behavior).
|
||||
|
||||
### Providing behaviors
|
||||
|
||||
Modules can provide their own behaviors with a couple of additional files.
|
||||
|
||||
The behavior itself is represented as a Drupal config entity, which gets
|
||||
installed as a YML config file during module installation.
|
||||
|
||||
For example (replace `my_module` with the module name, and `mybehavior` with
|
||||
the behavior name):
|
||||
|
||||
`my_module/config/install/farm_map.behavior.mybehavior.yml`
|
||||
|
||||
```yaml
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- my_module
|
||||
id: mybehavior
|
||||
label: My Behavior
|
||||
description: 'Adds my custom behavior logic.'
|
||||
library: 'my_module/behavior_mybehavior'
|
||||
settings: { }
|
||||
```
|
||||
|
||||
The module must declare the behavior JavaScript file as a "library" so that
|
||||
it can be included in the page(s) that need it.
|
||||
|
||||
For example (replace `my_module` with the module name, and `mybehavior` with
|
||||
the behavior name):
|
||||
|
||||
`my_module/my_module.libraries.yml`
|
||||
|
||||
```yaml
|
||||
behavior_mybehavior:
|
||||
js:
|
||||
js/farmOS.map.behaviors.mybehavior.js: { }
|
||||
dependencies:
|
||||
- farm_map/farm_map
|
||||
```
|
||||
|
||||
Finally, the behavior JavaScript file should have a path and filename that
|
||||
matches the library definition.
|
||||
|
||||
For example (replace `my_module` with the module name, and `mybehavior` with
|
||||
the behavior name):
|
||||
|
||||
`my_module/js/farmOS.map.behaviors.mybehavior.js`
|
||||
|
||||
```js
|
||||
(function () {
|
||||
farmOS.map.behaviors.mybehavior = {
|
||||
attach: function (instance) {
|
||||
|
||||
// My custom behavior logic.
|
||||
}
|
||||
};
|
||||
}());
|
||||
```
|
||||
|
||||
The `instance` object represents the farmOS-map instance, and includes helper
|
||||
methods for common needs (eg: `instance.addLayer()`), as well as direct access
|
||||
to the OpenLayers map object at `instance.map`.
|
||||
|
||||
For more information see:
|
||||
[github.com/farmOS/farmOS-map](https://github.com/farmOS/farmOS-map)
|
||||
|
||||
### Attaching behaviors
|
||||
|
||||
Behaviors can be "attached" (enabled) in a map in a few different ways:
|
||||
|
||||
- [Map types](#map-types) can include a list of default `behaviors`.
|
||||
- The `#behaviors` property of the `farm_map` [render element](#render-element)
|
||||
and `farm_map_input` [form element](#form-element) can add specific behaviors
|
||||
to individual elements.
|
||||
- A [render event](#render-events) subscriber can use the
|
||||
`$event->addBehavior()` method.
|
||||
|
||||
In all cases the behavior's `id` (as defined it its config entity YML) is used.
|
||||
|
||||
### Behavior settings
|
||||
|
||||
Some behaviors may require additional settings based on their context. Best
|
||||
practice is to include these in the map settings so that they are available in
|
||||
the behavior JavaScript in the following way:
|
||||
|
||||
`const settings = instance.farmMapSettings.behaviors.mybehavior;`
|
||||
|
||||
This can be accomplished in different ways, depending on how the behavior is
|
||||
being attached to the map.
|
||||
|
||||
[Map types](#map-types) can add behavior settings to their `options` property.
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- my_module
|
||||
id: mymaptype
|
||||
label: My Map Type
|
||||
description: "My module's custom map type."
|
||||
behaviors:
|
||||
- mybehavior
|
||||
options:
|
||||
behaviors:
|
||||
mybehavior:
|
||||
mysetting: True
|
||||
```
|
||||
|
||||
Maps added as [render](#render-element) or [form](#form-element) elements can
|
||||
add behavior settings in their `#map_settings` property. For example:
|
||||
|
||||
```php
|
||||
$build['mymap'] = [
|
||||
'#type' => 'farm_map',
|
||||
'#map_settings' => [
|
||||
'behaviors' => [
|
||||
'mybehavior' => [
|
||||
'mysetting' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'#behaviors' => [
|
||||
'mybehavior',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
Behaviors that are added via [render event](#render-events) subscribers can add
|
||||
settings at the same time:
|
||||
|
||||
```php
|
||||
$event->addBehavior('mybehavior', ['mysetting' => TRUE]);
|
||||
```
|
||||
|
||||
All of the above approaches will make the settings available in the behavior
|
||||
JavaScript in the same place.
|
||||
|
||||
## Render events
|
||||
|
||||
farmOS will trigger an event when a map is rendered. Modules can set up an
|
||||
event subscriber to perform additional logic at that time, such as adding
|
||||
behaviors.
|
||||
|
||||
For example, to add a behavior to all maps in farmOS, add the following two
|
||||
files (replace `my_module` with the module name, and `mybehavior` with the
|
||||
behavior name):
|
||||
|
||||
`my_module/my_module.services.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
my_module_map_render_event_subscriber:
|
||||
class: Drupal\my_module\EventSubscriber\MapRenderEventSubscriber
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
```
|
||||
|
||||
`my_module/src/EventSubscriber/MapRenderEventSubscriber`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Drupal\my_module\EventSubscriber;
|
||||
|
||||
use Drupal\farm_map\Event\MapRenderEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* An event subscriber for the MapRenderEvent.
|
||||
*/
|
||||
class MapRenderEventSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
return [
|
||||
MapRenderEvent::EVENT_NAME => 'onMapRender',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* React to the MapRenderEvent.
|
||||
*
|
||||
* @param \Drupal\farm_map\Event\MapRenderEvent $event
|
||||
* The MapRenderEvent.
|
||||
*/
|
||||
public function onMapRender(MapRenderEvent $event) {
|
||||
$event->addBehavior('mybehavior');
|
||||
}
|
||||
|
||||
}
|
||||
```
|
|
@ -37,6 +37,7 @@ nav:
|
|||
- Quick forms: development/module/quick.md
|
||||
- Entities: development/module/entities.md
|
||||
- Fields: development/module/fields.md
|
||||
- Maps: development/module/maps.md
|
||||
- OAuth: development/module/oauth.md
|
||||
- Roles: development/module/roles.md
|
||||
- Services: development/module/services.md
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- farm_map
|
||||
id: geofield
|
||||
label: Geofield
|
||||
description: 'Copies map layer data into a form textarea.'
|
||||
library: 'farm_map/behavior_geofield'
|
||||
settings: { }
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- farm_map
|
||||
id: input
|
||||
label: Input
|
||||
description: 'Syncs editable map layer data into a form input.'
|
||||
library: 'farm_map/behavior_input'
|
||||
settings: { }
|
|
@ -1,11 +0,0 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- farm_map
|
||||
id: geofield_widget
|
||||
label: Geofield widget
|
||||
description: 'Renders a geofield widget using farmOS-map.'
|
||||
behaviors: { }
|
||||
options: { }
|
|
@ -30,9 +30,9 @@ behavior_wkt:
|
|||
dependencies:
|
||||
- core/drupalSettings
|
||||
- farm_map/farm_map
|
||||
behavior_geofield:
|
||||
behavior_input:
|
||||
js:
|
||||
js/farmOS.map.behaviors.geofield.js: { }
|
||||
js/farmOS.map.behaviors.input.js: { }
|
||||
dependencies:
|
||||
- farm_map/farm_map
|
||||
behavior_enable_side_panel:
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update hooks for the farm_map module.
|
||||
*/
|
||||
|
||||
use Drupal\farm_map\Entity\MapBehavior;
|
||||
use Drupal\farm_map\Entity\MapType;
|
||||
|
||||
/**
|
||||
* Generalize geofield map types and behavior.
|
||||
*/
|
||||
function farm_map_post_update_generalize_geofield_map_types_behavior(&$sandbox) {
|
||||
|
||||
// Create the new input behavior.
|
||||
$input_behavior = MapBehavior::create([
|
||||
'id' => 'input',
|
||||
'label' => 'Input',
|
||||
'description' => 'Syncs editable map layer data into a form input.',
|
||||
'library' => 'farm_map/behavior_input',
|
||||
'settings' => [],
|
||||
'dependencies' => [
|
||||
'enforced' => [
|
||||
'module' => [
|
||||
'farm_map',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$input_behavior->save();
|
||||
|
||||
// Delete the geofield behavior.
|
||||
$geofield_behavior = MapBehavior::load('geofield');
|
||||
$geofield_behavior->delete();
|
||||
|
||||
// Delete the geofield_widget map type.
|
||||
$geofield_widget_map_type = MapType::load('geofield_widget');
|
||||
$geofield_widget_map_type->delete();
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
(function () {
|
||||
farmOS.map.behaviors.geofield = {
|
||||
farmOS.map.behaviors.input = {
|
||||
attach: function (instance) {
|
||||
instance.editAttached.then(() => {
|
||||
instance.edit.wktOn('featurechange', function(wkt) {
|
|
@ -24,6 +24,8 @@ class FarmMap extends RenderElement {
|
|||
],
|
||||
'#theme' => 'farm_map',
|
||||
'#map_type' => 'default',
|
||||
'#map_settings' => [],
|
||||
'#behaviors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -37,6 +39,8 @@ class FarmMap extends RenderElement {
|
|||
*
|
||||
* @return array
|
||||
* A renderable array representing the map.
|
||||
*
|
||||
* @see \Drupal\farm_map\Event\MapRenderEvent
|
||||
*/
|
||||
public static function preRenderMap(array $element) {
|
||||
|
||||
|
@ -66,14 +70,20 @@ class FarmMap extends RenderElement {
|
|||
$element['#attached']['library'][] = 'farm_map/farmOS-map';
|
||||
$element['#attached']['library'][] = 'farm_map/farm_map';
|
||||
|
||||
// Include map settings.
|
||||
$map_settings = !empty($element['#map_settings']) ? $element['#map_settings'] : [];
|
||||
// If #behaviors are included, attach each one.
|
||||
foreach ($element['#behaviors'] as $behavior_name) {
|
||||
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $behavior */
|
||||
$behavior = \Drupal::entityTypeManager()->getStorage('map_behavior')->load($behavior_name);
|
||||
if (!empty($behavior)) {
|
||||
$element['#attached']['library'][] = $behavior->getLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
// Include the map options.
|
||||
$map_options = $map->getMapOptions();
|
||||
|
||||
// Add the instance settings under the map id key.
|
||||
$instance_settings = array_merge_recursive($map_settings, $map_options);
|
||||
$instance_settings = array_merge_recursive($element['#map_settings'], $map_options);
|
||||
$element['#attached']['drupalSettings']['farm_map'][$map_id] = $instance_settings;
|
||||
|
||||
// Create and dispatch a MapRenderEvent.
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\farm_map\Element;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element\FormElement;
|
||||
use Drupal\geofield\GeoPHP\GeoPHPWrapper;
|
||||
|
||||
/**
|
||||
* Form element that returns WKT rendered in a map.
|
||||
*
|
||||
* @FormElement("farm_map_input")
|
||||
*/
|
||||
class FarmMapInput extends FormElement {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo() {
|
||||
$class = static::class;
|
||||
return [
|
||||
'#input' => TRUE,
|
||||
'#process' => [
|
||||
[$class, 'processElement'],
|
||||
],
|
||||
'#pre_render' => [
|
||||
[$class, 'preRenderGroup'],
|
||||
],
|
||||
'#element_validate' => [
|
||||
[$class, 'elementValidate'],
|
||||
],
|
||||
'#theme_wrappers' => ['form_element'],
|
||||
// Display descriptions above the map by default.
|
||||
'#description_display' => 'before',
|
||||
'#map_type' => 'default',
|
||||
'#map_settings' => [],
|
||||
'#behaviors' => [],
|
||||
'#default_value' => '',
|
||||
'#display_raw_geometry' => FALSE,
|
||||
'#disabled' => FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the form element.
|
||||
*
|
||||
* @param array $element
|
||||
* An associative array containing the properties and children of the
|
||||
* element. Note that $element must be taken by reference here, so processed
|
||||
* child elements are taken over into $form_state.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param array $complete_form
|
||||
* The complete form structure.
|
||||
*
|
||||
* @return array
|
||||
* The processed element.
|
||||
*/
|
||||
public static function processElement(array $element, FormStateInterface $form_state, array &$complete_form) {
|
||||
$element['#tree'] = TRUE;
|
||||
|
||||
// Merge provided map behaviors into defaults. Enable wkt and input
|
||||
// behaviors if #disabled is not TRUE.
|
||||
$default_behaviors = !$element['#disabled'] ? ['wkt', 'input'] : [];
|
||||
$behaviors = array_merge($default_behaviors, $element['#behaviors']);
|
||||
|
||||
// Recursively merge provided map settings into defaults.
|
||||
$map_settings = array_merge_recursive([
|
||||
'behaviors' => [
|
||||
'wkt' => [
|
||||
'edit' => !$element['#disabled'],
|
||||
'zoom' => TRUE,
|
||||
],
|
||||
],
|
||||
], $element['#map_settings']);
|
||||
|
||||
// Define the map render array.
|
||||
$element['map'] = [
|
||||
'#type' => 'farm_map',
|
||||
'#map_type' => $element['#map_type'],
|
||||
'#map_settings' => $map_settings,
|
||||
'#behaviors' => $behaviors,
|
||||
];
|
||||
|
||||
// Add a textarea for the WKT value.
|
||||
$display_raw_geometry = $element['#display_raw_geometry'];
|
||||
$element_title = $element['#title'] ?? t('Geometry');
|
||||
$element['value'] = [
|
||||
'#type' => $display_raw_geometry ? 'textarea' : 'hidden',
|
||||
'#title' => $element_title . ' ' . t('WKT'),
|
||||
'#title_display' => 'invisible',
|
||||
'#attributes' => [
|
||||
'data-map-geometry-field' => TRUE,
|
||||
],
|
||||
'#disabled' => $element['#disabled'],
|
||||
];
|
||||
|
||||
// Add default value if provided.
|
||||
if (!empty($element['#default_value'])) {
|
||||
$element['map']['#map_settings']['wkt'] = $element['#default_value'];
|
||||
$element['value']['#default_value'] = $element['#default_value'];
|
||||
}
|
||||
|
||||
// Return the element.
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the form element.
|
||||
*/
|
||||
public static function elementValidate(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
|
||||
// Validate that the geometry data is valid by attempting to load it into
|
||||
// GeoPHP. This uses the same logic and error message as the geofield
|
||||
// module's validation constraint.
|
||||
// @see Drupal\geofield\Plugin\Validation\Constraint\GeoConstraint
|
||||
// @see Drupal\geofield\Plugin\Validation\Constraint\GeoConstraintValidator
|
||||
$value = $element['value']['#value'];
|
||||
if (!empty($value)) {
|
||||
$geophp = new GeoPHPWrapper();
|
||||
$valid_geometry = TRUE;
|
||||
try {
|
||||
if (!$geophp->load($value)) {
|
||||
$valid_geometry = FALSE;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$valid_geometry = FALSE;
|
||||
}
|
||||
if (!$valid_geometry) {
|
||||
$form_state->setError($element, t('"@value" is not a valid geospatial content.', ['@value' => $value]));
|
||||
}
|
||||
}
|
||||
|
||||
// Save the WKT string value to the overall element.
|
||||
$form_state->setValueForElement($element, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
|
||||
if ($input === FALSE) {
|
||||
return $element['#default_value'] ?: '';
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
|
@ -72,6 +72,8 @@ class MapRenderEvent extends Event {
|
|||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
|
||||
*
|
||||
* @see \Drupal\farm_map\Element\FarmMap
|
||||
*/
|
||||
public function addBehavior(string $behavior_name, array $settings = []) {
|
||||
|
||||
|
|
|
@ -61,12 +61,6 @@ class MapRenderEventSubscriber implements EventSubscriberInterface {
|
|||
$event->addBehavior('wkt');
|
||||
}
|
||||
|
||||
// Add the wkt and geofield behavior to the geofield_widget map.
|
||||
if (in_array($event->getMapType()->id(), ['geofield_widget'])) {
|
||||
$event->addBehavior('wkt');
|
||||
$event->addBehavior('geofield');
|
||||
}
|
||||
|
||||
// Get whether the side panel should be enabled.
|
||||
$enable_side_panel = $this->configFactory->get('farm_map.settings')->get('enable_side_panel');
|
||||
|
||||
|
|
|
@ -133,45 +133,30 @@ class GeofieldWidget extends GeofieldBaseWidget {
|
|||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
|
||||
|
||||
// Wrap the map in a collapsible details element.
|
||||
// Use the farm_map_input form element.
|
||||
$element['#type'] = 'farm_map_input';
|
||||
|
||||
// Use the geofield map type.
|
||||
$element['#map_type'] = 'geofield';
|
||||
|
||||
// Wrap in a fieldset.
|
||||
$element['#theme_wrappers'] = ['fieldset'];
|
||||
|
||||
// Wrap the map with a unique id for populating from files.
|
||||
$field_name = $this->fieldDefinition->getName();
|
||||
$field_wrapper_id = Html::getUniqueId($field_name . '_wrapper');
|
||||
$element['#type'] = 'details';
|
||||
$element['#title'] = $this->t('Geometry');
|
||||
$element['#open'] = TRUE;
|
||||
$element['#prefix'] = '<div id="' . $field_wrapper_id . '">';
|
||||
$element['#suffix'] = '</div>';
|
||||
|
||||
// Get the current form state value. Prioritize form state over field value.
|
||||
$form_value = $form_state->getValue([$field_name, $delta, 'value']);
|
||||
$form_value = $form_state->getValue([$field_name, $delta]);
|
||||
$field_value = $items[$delta]->value;
|
||||
$current_value = $form_value ?? $field_value;
|
||||
$current_value = $form_value['value'] ?? $field_value;
|
||||
$element['#default_value'] = $current_value;
|
||||
|
||||
// Define the map render array.
|
||||
$element['map'] = [
|
||||
'#type' => 'farm_map',
|
||||
'#map_type' => 'geofield_widget',
|
||||
'#map_settings' => [
|
||||
'wkt' => $current_value,
|
||||
'behaviors' => [
|
||||
'wkt' => [
|
||||
'edit' => TRUE,
|
||||
'zoom' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Add a textarea for the WKT value.
|
||||
// Configure to display raw geometry.
|
||||
$display_raw_geometry = $this->getSetting('display_raw_geometry');
|
||||
$element['value'] = [
|
||||
'#type' => $display_raw_geometry ? 'textarea' : 'hidden',
|
||||
'#title' => $this->t('Geometry'),
|
||||
'#default_value' => $current_value,
|
||||
'#attributes' => [
|
||||
'data-map-geometry-field' => TRUE,
|
||||
],
|
||||
];
|
||||
$element['#display_raw_geometry'] = $display_raw_geometry;
|
||||
|
||||
// Add an option to populate geometry using files field.
|
||||
// The "populate_file_field" field setting must be configured and the
|
||||
|
@ -192,9 +177,16 @@ class GeofieldWidget extends GeofieldBaseWidget {
|
|||
':input[name="' . $populate_file_field . '[0][fids]"]' => ['empty' => TRUE],
|
||||
],
|
||||
],
|
||||
'#weight' => 10,
|
||||
];
|
||||
}
|
||||
|
||||
// Override the element validation to prevent transformation of the value
|
||||
// from array to string, and because Geofields already perform the same
|
||||
// geometry validation.
|
||||
// @see \Drupal\geofield\Plugin\Validation\GeoConstraintValidator.
|
||||
$element['#element_validate'] = [];
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
|
@ -281,11 +273,11 @@ class GeofieldWidget extends GeofieldBaseWidget {
|
|||
$field_name = $this->fieldDefinition->getName();
|
||||
$delta = $element['#delta'];
|
||||
$user_input = $form_state->getUserInput();
|
||||
unset($user_input[$field_name][$delta]['value']);
|
||||
unset($user_input[$field_name][$delta]);
|
||||
$form_state->setUserInput($user_input);
|
||||
|
||||
// Set the new form value.
|
||||
$form_state->setValue([$field_name, $delta, 'value'], $wkt);
|
||||
$form_state->setValue([$field_name, $delta], ['value' => $wkt]);
|
||||
|
||||
// Rebuild the form so the map widget is rebuilt with the new value.
|
||||
$form_state->setRebuild(TRUE);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
langcode: en
|
||||
status: true
|
||||
id: test
|
||||
label: Test
|
||||
description: ''
|
||||
name_pattern: 'Test log [log:id]'
|
||||
workflow: log_default
|
||||
new_revision: true
|
|
@ -0,0 +1,10 @@
|
|||
name: farmOS map tests
|
||||
type: module
|
||||
description: Support module for farmOS map testing.
|
||||
package: Testing
|
||||
core_version_requirement: ^8.8 || ^9
|
||||
dependencies:
|
||||
- farm:farm_location
|
||||
- farm:farm_map
|
||||
- farm:farm_quick
|
||||
- log:log
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\farm_map_test\Plugin\QuickForm;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
|
||||
use Drupal\farm_quick\Traits\QuickLogTrait;
|
||||
|
||||
/**
|
||||
* Test quick form.
|
||||
*
|
||||
* @QuickForm(
|
||||
* id = "test",
|
||||
* label = @Translation("Test quick form"),
|
||||
* description = @Translation("Test quick form description."),
|
||||
* helpText = @Translation("Test quick form help text."),
|
||||
* permissions = {
|
||||
* "create test log",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class Test extends QuickFormBase {
|
||||
|
||||
use QuickLogTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
|
||||
// Geometry field with a default value and no raw data textfield.
|
||||
$form['geometry1'] = [
|
||||
'#type' => 'farm_map_input',
|
||||
'#title' => $this->t('Geometry 1'),
|
||||
'#display_raw_geometry' => FALSE,
|
||||
'#default_value' => 'POINT(-42.689862437640826 32.621823310499934)',
|
||||
];
|
||||
|
||||
// Geometry field without a default value and a raw data field.
|
||||
$form['geometry2'] = [
|
||||
'#type' => 'farm_map_input',
|
||||
'#title' => $this->t('Geometry 2'),
|
||||
'#display_raw_geometry' => TRUE,
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
|
||||
// Create two logs.
|
||||
$this->createLog([
|
||||
'type' => 'test',
|
||||
'name' => 'Test 1',
|
||||
'geometry' => $form_state->getValue('geometry1'),
|
||||
]);
|
||||
$this->createLog([
|
||||
'type' => 'test',
|
||||
'name' => 'Test 2',
|
||||
'geometry' => $form_state->getValue('geometry2'),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\farm_map\Functional;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Tests\farm_test\Functional\FarmBrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the farmOS map form element.
|
||||
*
|
||||
* @group farm
|
||||
*/
|
||||
class MapFormTest extends FarmBrowserTestBase {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'farm_map_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* Test the farmOS map form element.
|
||||
*/
|
||||
public function testMapForm() {
|
||||
|
||||
// Create and login a test user with permission to create test logs.
|
||||
$user = $this->createUser(['create test log']);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Go to the test quick form and confirm that both of the geometry fields
|
||||
// are visible, and only the second field's WKT text field is visible.
|
||||
$this->drupalGet('quick/test');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains($this->t('Geometry 1'));
|
||||
$this->assertSession()->pageTextContains($this->t('Geometry 2'));
|
||||
$this->assertSession()->pageTextNotContains($this->t('Geometry 1 WKT'));
|
||||
$this->assertSession()->pageTextContains($this->t('Geometry 2 WKT'));
|
||||
|
||||
// Submit the form with a value for the second geometry.
|
||||
$edit = ['geometry2[value]' => 'POINT(-45.967095060886315 32.77503850904169)'];
|
||||
$this->submitForm($edit, 'Submit');
|
||||
|
||||
// Load logs.
|
||||
$logs = \Drupal::entityTypeManager()->getStorage('log')->loadMultiple();
|
||||
|
||||
// Confirm that two logs were created.
|
||||
$this->assertCount(2, $logs);
|
||||
|
||||
// Check that the first log's geometry was populated with the form field's
|
||||
// default value.
|
||||
$log = $logs[1];
|
||||
$this->assertEquals('POINT(-42.689862437640826 32.621823310499934)', $log->get('geometry')->value);
|
||||
|
||||
// Check that the second log's geometry field was populated with the value
|
||||
// entered into the form.
|
||||
$log = $logs[2];
|
||||
$this->assertEquals('POINT(-45.967095060886315 32.77503850904169)', $log->get('geometry')->value);
|
||||
|
||||
// Test that submitting an invalid geometry throws a form validation error.
|
||||
$this->drupalGet('quick/test');
|
||||
$edit = ['geometry2[value]' => 'POLYGON()'];
|
||||
$this->submitForm($edit, 'Submit');
|
||||
$this->assertSession()->pageTextContains($this->t('"POLYGON()" is not a valid geospatial content.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -65,7 +65,7 @@ class MapRenderEventSubscriber implements EventSubscriberInterface {
|
|||
$map_id = $event->getmapType()->id();
|
||||
|
||||
// Add behaviors/settings to default and geofield maps.
|
||||
if (in_array($map_id, ['default', 'geofield', 'geofield_widget'])) {
|
||||
if (in_array($map_id, ['default', 'geofield'])) {
|
||||
|
||||
// Add "All locations" layers.
|
||||
$event->addBehavior('asset_type_layers');
|
||||
|
|
Loading…
Reference in New Issue