diff --git a/CHANGELOG.md b/CHANGELOG.md index 18316a526..c640940f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/docs/development/module/maps.md b/docs/development/module/maps.md new file mode 100644 index 000000000..3089772d4 --- /dev/null +++ b/docs/development/module/maps.md @@ -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 + 'onMapRender', + ]; + } + + /** + * React to the MapRenderEvent. + * + * @param \Drupal\farm_map\Event\MapRenderEvent $event + * The MapRenderEvent. + */ + public function onMapRender(MapRenderEvent $event) { + $event->addBehavior('mybehavior'); + } + +} +``` diff --git a/mkdocs.yml b/mkdocs.yml index ec71d2a4b..b6acd8efa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/modules/core/map/config/install/farm_map.map_behavior.geofield.yml b/modules/core/map/config/install/farm_map.map_behavior.geofield.yml deleted file mode 100644 index f4f0897ca..000000000 --- a/modules/core/map/config/install/farm_map.map_behavior.geofield.yml +++ /dev/null @@ -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: { } diff --git a/modules/core/map/config/install/farm_map.map_behavior.input.yml b/modules/core/map/config/install/farm_map.map_behavior.input.yml new file mode 100644 index 000000000..df1acdd0b --- /dev/null +++ b/modules/core/map/config/install/farm_map.map_behavior.input.yml @@ -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: { } diff --git a/modules/core/map/config/install/farm_map.map_type.geofield_widget.yml b/modules/core/map/config/install/farm_map.map_type.geofield_widget.yml deleted file mode 100644 index e8fd56035..000000000 --- a/modules/core/map/config/install/farm_map.map_type.geofield_widget.yml +++ /dev/null @@ -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: { } diff --git a/modules/core/map/farm_map.libraries.yml b/modules/core/map/farm_map.libraries.yml index f286062c4..50db0321e 100644 --- a/modules/core/map/farm_map.libraries.yml +++ b/modules/core/map/farm_map.libraries.yml @@ -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: diff --git a/modules/core/map/farm_map.post_update.php b/modules/core/map/farm_map.post_update.php new file mode 100644 index 000000000..c64245074 --- /dev/null +++ b/modules/core/map/farm_map.post_update.php @@ -0,0 +1,40 @@ + '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(); +} diff --git a/modules/core/map/js/farmOS.map.behaviors.geofield.js b/modules/core/map/js/farmOS.map.behaviors.input.js similarity index 91% rename from modules/core/map/js/farmOS.map.behaviors.geofield.js rename to modules/core/map/js/farmOS.map.behaviors.input.js index 6b4b1665a..127de36e1 100644 --- a/modules/core/map/js/farmOS.map.behaviors.geofield.js +++ b/modules/core/map/js/farmOS.map.behaviors.input.js @@ -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) { diff --git a/modules/core/map/src/Element/FarmMap.php b/modules/core/map/src/Element/FarmMap.php index 7f2b795fa..8f9ae48e7 100644 --- a/modules/core/map/src/Element/FarmMap.php +++ b/modules/core/map/src/Element/FarmMap.php @@ -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. diff --git a/modules/core/map/src/Element/FarmMapInput.php b/modules/core/map/src/Element/FarmMapInput.php new file mode 100644 index 000000000..a8faf5d61 --- /dev/null +++ b/modules/core/map/src/Element/FarmMapInput.php @@ -0,0 +1,149 @@ + 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; + } + +} diff --git a/modules/core/map/src/Event/MapRenderEvent.php b/modules/core/map/src/Event/MapRenderEvent.php index 3073cb2b8..5829ad075 100644 --- a/modules/core/map/src/Event/MapRenderEvent.php +++ b/modules/core/map/src/Event/MapRenderEvent.php @@ -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 = []) { diff --git a/modules/core/map/src/EventSubscriber/MapRenderEventSubscriber.php b/modules/core/map/src/EventSubscriber/MapRenderEventSubscriber.php index 71ef0de42..783aed7c5 100644 --- a/modules/core/map/src/EventSubscriber/MapRenderEventSubscriber.php +++ b/modules/core/map/src/EventSubscriber/MapRenderEventSubscriber.php @@ -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'); diff --git a/modules/core/map/src/Plugin/Field/FieldWidget/GeofieldWidget.php b/modules/core/map/src/Plugin/Field/FieldWidget/GeofieldWidget.php index 13849d72a..dfab211c8 100644 --- a/modules/core/map/src/Plugin/Field/FieldWidget/GeofieldWidget.php +++ b/modules/core/map/src/Plugin/Field/FieldWidget/GeofieldWidget.php @@ -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'] = '
'; $element['#suffix'] = '
'; // 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); diff --git a/modules/core/map/tests/modules/farm_map_test/config/install/log.type.test.yml b/modules/core/map/tests/modules/farm_map_test/config/install/log.type.test.yml new file mode 100644 index 000000000..d88475de0 --- /dev/null +++ b/modules/core/map/tests/modules/farm_map_test/config/install/log.type.test.yml @@ -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 diff --git a/modules/core/map/tests/modules/farm_map_test/farm_map_test.info.yml b/modules/core/map/tests/modules/farm_map_test/farm_map_test.info.yml new file mode 100644 index 000000000..f48fb9a88 --- /dev/null +++ b/modules/core/map/tests/modules/farm_map_test/farm_map_test.info.yml @@ -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 diff --git a/modules/core/map/tests/modules/farm_map_test/src/Plugin/QuickForm/Test.php b/modules/core/map/tests/modules/farm_map_test/src/Plugin/QuickForm/Test.php new file mode 100644 index 000000000..2138f812b --- /dev/null +++ b/modules/core/map/tests/modules/farm_map_test/src/Plugin/QuickForm/Test.php @@ -0,0 +1,67 @@ + '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'), + ]); + } + +} diff --git a/modules/core/map/tests/src/Functional/MapFormTest.php b/modules/core/map/tests/src/Functional/MapFormTest.php new file mode 100644 index 000000000..34693b5d5 --- /dev/null +++ b/modules/core/map/tests/src/Functional/MapFormTest.php @@ -0,0 +1,69 @@ +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.')); + } + +} diff --git a/modules/core/ui/map/src/EventSubscriber/MapRenderEventSubscriber.php b/modules/core/ui/map/src/EventSubscriber/MapRenderEventSubscriber.php index 1683bdeff..1d75f4720 100644 --- a/modules/core/ui/map/src/EventSubscriber/MapRenderEventSubscriber.php +++ b/modules/core/ui/map/src/EventSubscriber/MapRenderEventSubscriber.php @@ -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');