clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map
id: enable_side_panel
label: Enable Side Panel
description: 'Enables the map side panel and moves the layer switcher into it.'
library: 'farm_map/behavior_enable_side_panel'
settings: { }

View File

@@ -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: { }

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map
id: popup
label: Popup
description: 'Creates popups on the map when features are selected.'
library: 'farm_map/behavior_popup'
settings: { }

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map
id: wkt
label: WKT
description: 'Displays WKT as a map layer.'
library: 'farm_map/behavior_wkt'
settings: { }

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map
id: default
label: Default
description: 'The default farmOS map.'
behaviors: { }
options: { }

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map
id: geofield
label: Geofield
description: 'Renders geofield using farmOS-map.'
behaviors: { }
options: { }

View File

@@ -0,0 +1 @@
enable_side_panel: true

View File

@@ -0,0 +1,97 @@
# Schema for the configuration files of the farm_map module.
farm_map.map_type.*:
type: config_entity
label: 'Map'
mapping:
id:
type: string
label: 'Machine-readable name'
label:
type: label
label: 'Type'
description:
type: text
label: 'Description'
behaviors:
type: sequence
label: 'Map behaviors'
nullable: true
sequence:
type: string
label: 'Map behavior id'
options:
type: sequence
label: 'farmOS-map options'
sequence:
type: ignore
label: 'Option name'
farm_map.map_behavior.*:
type: config_entity
label: 'Map behavior'
mapping:
id:
type: string
label: 'Machine-readable name'
label:
type: label
label: 'Behavior'
description:
type: text
label: 'Description'
library:
type: string
label: 'Library name'
settings:
type: sequence
label: 'Behavior settings'
sequence:
type: ignore
label: 'Behavior setting'
farm_map.layer_style.*:
type: config_entity
label: 'Map layer style'
mapping:
id:
type: string
label: 'Machine-readable name'
color:
type: string
label: 'farmOS-map color'
conditions:
type: sequence
label: 'Style matching conditions'
sequence:
type: ignore
label: 'Layer style setting'
field.widget.settings.farm_map_geofield:
type: mapping
label: 'Farm map geofield settings'
mapping:
display_raw_geometry:
type: boolean
label: 'Whether or not to display the raw geometry field underneath map.'
populate_file_field:
type: string
label: 'The name of the file field to geocode into the geofield.'
block.settings.map_block:
type: block_settings
label: 'map block settings'
mapping:
map_type:
type: string
label: 'Map type'
map_behaviors:
type: sequence
label: 'Map behaviors'
nullable: true
sequence:
type: string
label: 'Map behavior id'
farm_map.settings:
type: config_object
label: 'Map Settings'
mapping:
enable_side_panel:
type: boolean
label: 'Enable Side Panel'
description: 'Enables the side panel in farmOS maps for displaying additional settings and information.'

View File

@@ -0,0 +1,10 @@
.farm-map {
height: 400px;
resize: vertical;
overflow: hidden;
}
/* Inline labels cause the map to cover other fields. */
.field--label-inline .farm-map {
clear: both;
}

View File

@@ -0,0 +1,4 @@
/* Remove the popup name margin. */
.farm-map .ol-popup-name {
margin: 0;
}

View File

@@ -0,0 +1,7 @@
name: farmOS Map
description: Provides farmOS map features.
type: module
package: farmOS
core_version_requirement: ^10
dependencies:
- drupal:block

View File

@@ -0,0 +1,58 @@
farm_map_public_path:
js:
js/farm_map_public_path.js: { }
dependencies:
- core/drupalSettings
farmOS-map:
remote: https://github.com/farmOS/farmOS-map
license:
name: MIT
url: https://github.com/farmOS/farmOS-map/blob/master/LICENSE
gpl-compatible: true
js:
/libraries/farmOS-map/farmOS-map.js:
# Skip aggregating farmOS-map.js with other JS since that
# breaks the lazy loading of behavior chunks.
preprocess: false
minified: true
css:
theme:
/libraries/farmOS-map/farmOS-map.css: { }
dependencies:
- core/drupalSettings
- farm_map/farm_map_public_path
farm_map:
css:
theme:
css/farm_map.css: { }
js:
js/farm_map.js: { }
dependencies:
- core/drupalSettings
- farm_map/farmOS-map
behavior_wkt:
js:
js/farmOS.map.behaviors.wkt.js: { }
dependencies:
- core/drupalSettings
- farm_map/farm_map
behavior_input:
js:
js/farmOS.map.behaviors.input.js: { }
dependencies:
- farm_map/farm_map
behavior_enable_side_panel:
js:
js/farmOS.map.behaviors.enable_side_panel.js: { }
dependencies:
- farm_map/farm_map
behavior_popup:
js:
js/farmOS.map.behaviors.popup.js: { }
css:
theme:
css/map_popup.css: { }
dependencies:
- farm_map/farm_map

View File

@@ -0,0 +1,12 @@
farm_map.settings:
base_route: farm_settings.settings_page
route_name: farm_map.settings
title: 'Map'
weight: 5
# Provide a default sub tab.
farm_map.settings_sub:
title: 'Map'
route_name: farm_map.settings
parent_id: farm_map.settings
weight: -5

View File

@@ -0,0 +1,19 @@
<?php
/**
* @file
* The farm_map module.
*/
/**
* Implements hook_theme().
*/
function farm_map_theme($existing, $type, $theme, $path) {
return [
'farm_map' => [
'variables' => [
'attributes' => [],
],
],
];
}

View File

@@ -0,0 +1,2 @@
administer farm map:
title: 'Administer farmOS map'

View File

@@ -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();
}

View File

@@ -0,0 +1,7 @@
farm_map.settings:
path: '/farm/settings/map'
defaults:
_form: '\Drupal\farm_map\Form\MapSettingsForm'
_title: 'Map settings'
requirements:
_permission: 'administer farm map'

View File

@@ -0,0 +1,9 @@
services:
farm_map.map_render_event_subscriber:
class: Drupal\farm_map\EventSubscriber\MapRenderEventSubscriber
arguments:
- '@config.factory'
tags:
- { name: 'event_subscriber' }
farm_map.layer_style_loader:
class: Drupal\farm_map\LayerStyleLoader

View File

@@ -0,0 +1,7 @@
(function () {
// Make the built-in farmOS-map 'sidePanel' behavior attach on map instantiation
farmOS.map.behaviors.sidePanel = farmOS.map.namedBehaviors.sidePanel;
// Make the built-in farmOS-map 'layerSwitcherInSidePanel' behavior attach on map instantiation
farmOS.map.behaviors.layerSwitcherInSidePanel = farmOS.map.namedBehaviors.layerSwitcherInSidePanel;
}());

View File

@@ -0,0 +1,42 @@
(function () {
farmOS.map.behaviors.input = {
attach: function (instance) {
// Get the data input form element.
var input = instance.map.getTargetElement().parentElement.querySelector('[data-map-geometry-field]')
// When features change in the edit layer, write WKT to the input form element.
instance.editAttached && instance.editAttached.then(() => {
instance.edit.wktOn('featurechange', function(wkt) {
if (input.value !== wkt) {
input.value = wkt;
}
});
});
// Add an event listener to the input element, which will attempt to
// import new WKT when it changes.
input.addEventListener('input', (e) => {
// If the value is empty, only clear features from the layer.
if (!input.value) {
instance.edit.layer.getSource().clear();
return;
}
// Clear features from the layer.
instance.edit.layer.getSource().clear();
// Read features from WKT and add them to the layer.
var features = instance.readFeatures('wkt', input.value);
instance.edit.layer.getSource().addFeatures(features);
// Zoom to the layer.
instance.zoomToLayer(instance.edit.layer);
});
},
// Make sure this runs after farmOS.map.behaviors.wkt.
weight: 101,
};
}());

View File

@@ -0,0 +1,69 @@
(function () {
farmOS.map.behaviors.popup = {
attach: function (instance) {
// Helper function to build the feature name as a link.
const featureName = function (feature) {
// Bail if either the name or url aren't defined.
const name = feature.get('name');
const url = feature.get('url');
if (name === undefined || url === undefined) {
return name;
}
// Build a link with the url and name.
return `<a href="${url}">${name}</a>`;
}
// Create a popup and add it to the instance for future reference.
instance.popup = instance.addPopup(function (event) {
var content = '';
// Get all features at the point that was clicked and sort them by area from smallest to largest.
// @todo GeometryCollections have an area of 0, which can cause some undesirable behavior.
var clickedFeatures = instance.map.getFeaturesAtPixel(event.pixel);
const sortValue = feature => typeof feature.getGeometry().getArea === 'function' ? feature.getGeometry().getArea() : 0;
clickedFeatures.sort((a,b) => sortValue(a) - sortValue(b))
// Get the first clicked feature.
var feature = clickedFeatures[0];
if (feature) {
// If the feature is a cluster, then create a list of names and add it
// to the overall feature's description.
var features = feature.get('features');
if (features !== undefined) {
var names = [];
features.forEach(function (item) {
const name = featureName(item);
if (name !== undefined) {
names.push(name);
}
});
if (names.length !== 0) {
feature.set('description', '<ul><li>' + names.join('</li><li>') + '</li></ul>');
}
feature.set('name', names.length + ' item(s):');
}
// A popup name is required.
var name = featureName(feature) || '';
if (name !== '') {
// Get the description and measurement.
var description = feature.get('description') || '';
var measurement = instance.measureGeometry(feature.getGeometry(), instance.units);
// Build content with all three values, even if empty. The measurement and description divs may be used
// as placeholders for map behaviors to place additional information.
content = '<h4 class="ol-popup-name">' + name + '</h4><div class="ol-popup-measurement"><small>' + measurement + '</small></div><div class="ol-popup-description">' + description + '</div>';
}
}
return content;
});
},
// Make sure this runs early so other behaviors can dispatch popup events
// with instance.popup.on().
weight: -100,
};
}());

View File

@@ -0,0 +1,57 @@
(function () {
farmOS.map.behaviors.wkt = {
attach: function (instance) {
const settings = instance.farmMapSettings;
// If WKT was set, create a layer.
if (settings.wkt) {
var wkt = settings.wkt;
var type = 'vector';
var opts = {
title: 'Geometry',
color: 'orange',
};
if (wkt !== '' && wkt !== 'GEOMETRYCOLLECTION EMPTY') {
type = 'wkt';
opts.wkt = wkt;
}
var layer = instance.addLayer(type, opts);
}
// Variable used to track the layer to add measurements to and to zoom in on.
// It is either immediately the layer created with passed-in WKT data or is later
// resolved to the edit layer once the edit behavior is fully attached.
var focusLayerPromise = Promise.resolve(layer);
// If edit is true, enable drawing controls.
if (settings.behaviors && settings.behaviors.wkt && settings.behaviors.wkt.edit) {
if (layer !== undefined) {
instance.editAttached = instance.addBehavior('edit', { layer: layer });
} else {
instance.editAttached = instance.addBehavior('edit');
// Focus on the edit layer if no layer was provided
focusLayerPromise = instance.editAttached
.then(() => instance.edit.layer);
}
// Add the snappingGrid behavior.
instance.addBehavior('snappingGrid');
}
focusLayerPromise.then(focusLayer => {
// Enable the line/polygon measure behavior.
instance.addBehavior('measure', { layer: focusLayer });
// If the layer has features, zoom to them.
// Otherwise, zoom to all vectors.
if (focusLayer !== undefined) {
instance.zoomToLayer(focusLayer);
} else {
instance.zoomToVectors();
}
});
},
weight: 100,
};
}());

View File

@@ -0,0 +1,57 @@
(function (Drupal, drupalSettings) {
Drupal.behaviors.farm_map = {
attach: function (context, settings) {
context.querySelectorAll('[data-map-instantiator="farm_map"]').forEach(function (element) {
// Only create a map once per element.
if (element.getAttribute('processed')) return;
element.setAttribute('processed', true);
element.setAttribute('tabIndex', 0);
const drupalSettingsKey = element.getAttribute('id');
const mapInstanceOptions = {};
Drupal.behaviors.farm_map.createMapInstance(context, element, drupalSettingsKey, mapInstanceOptions);
});
},
createMapInstance: function(context, element, drupalSettingsKey, mapInstanceOptions) {
// Get the units.
let units = 'metric';
if (!!drupalSettings.farm_map.units) {
units = drupalSettings.farm_map.units;
}
// Build default options.
const defaultOptions = {
units,
interactions: {
onFocusOnly: true
},
};
const mapOptions = {
...defaultOptions,
...drupalSettings.farm_map[drupalSettingsKey].instance,
...mapInstanceOptions
};
const instance = farmOS.map.create(element, mapOptions);
// Expose settings on the instance so other behaviors don't need to know how to look them up in drupalSettings
instance.farmMapSettings = drupalSettings.farm_map[drupalSettingsKey] || {};
context.querySelectorAll('.ol-popup-closer').forEach(function (element) {
element.onClick = function (element) {
element.focus();
};
});
// Update the map size when the wrapper element is resized.
new ResizeObserver(() => instance.map.updateSize()).observe(element);
return instance;
}
};
}(Drupal, drupalSettings));

View File

@@ -0,0 +1,6 @@
(function (drupalSettings) {
// Set window.farmosMapPublicPath to tell farmOS-map where to load chunks from.
if (!!drupalSettings.farm_map_public_path) {
window.farmosMapPublicPath = drupalSettings.farm_map_public_path;
}
}(drupalSettings));

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_map_mapbox
id: mapbox
label: Mapbox
description: 'Displays Mapbox layers.'
library: 'farm_map_mapbox/mapbox'
settings: { }

View File

@@ -0,0 +1,8 @@
farm_map_mapbox.settings:
type: config_object
label: 'Mapbox Settings'
mapping:
api_key:
type: string
label: 'Mapbox API Key'
description: 'The API key used to request Mapbox imagery layers.'

View File

@@ -0,0 +1,7 @@
name: farmOS Mapbox
description: Adds Mapbox layers to farmOS maps.
type: module
package: farmOS Maps
core_version_requirement: ^10
dependencies:
- farm:farm_map

View File

@@ -0,0 +1,6 @@
mapbox:
js:
js/farmOS.map.behaviors.mapbox.js: { }
dependencies:
- core/drupalSettings
- farm_map/farmOS-map

View File

@@ -0,0 +1,4 @@
farm_map_mapbox.settings:
title: 'Mapbox'
route_name: farm_map_mapbox.settings
parent_id: farm_map.settings

View File

@@ -0,0 +1,7 @@
farm_map_mapbox.settings:
path: 'farm/settings/map/mapbox'
defaults:
_form: '\Drupal\farm_map_mapbox\Form\MapboxSettingsForm'
_title: 'Mapbox settings'
requirements:
_permission: 'administer farm map'

View File

@@ -0,0 +1,7 @@
services:
mapbox_map_render_event_subscriber:
class: Drupal\farm_map_mapbox\EventSubscriber\MapRenderEventSubscriber
arguments:
- '@config.factory'
tags:
- { name: 'event_subscriber' }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\farm_map_mapbox\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\farm_map\Event\MapRenderEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber for the MapRenderEvent.
*
* This adds the mapbox behavior and api_key setting to all maps.
*/
class MapRenderEventSubscriber implements EventSubscriberInterface {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
private $configFactory;
/**
* Constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
MapRenderEvent::EVENT_NAME => 'onMapRender',
];
}
/**
* React to the MapRenderEvent.
*
* @param \Drupal\farm_map\Event\MapRenderEvent $event
* The MapRenderEvent.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function onMapRender(MapRenderEvent $event) {
// Get the mapbox api_key.
$api_key = $this->configFactory->get('farm_map_mapbox.settings')->get('api_key');
// Set a cache tag on the mapbox settings in case this ever changes.
// This is added to all maps since the mapbox behavior can be added to all
// maps.
$event->addCacheTags(['config:farm_map_mapbox.settings']);
// If the api key exists, add the mapbox behavior.
if (!empty($api_key)) {
$event->addBehavior('mapbox', ['api_key' => $api_key]);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\farm_map_mapbox\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a mapbox settings form.
*/
class MapboxSettingsForm extends ConfigFormbase {
/**
* Config settings.
*
* @var string
*/
const SETTINGS = 'farm_map_mapbox.settings';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'farm_map_mapbox_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
static::SETTINGS,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateinterface $form_state) {
$config = $this->config(static::SETTINGS);
// Add api_key field.
$form['api_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Mapbox API Key'),
'#description' => $this->t('Enter your Mapbox API key.'),
'#default_value' => $config->get('api_key'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->configFactory->getEditable(static::SETTINGS)
->set('api_key', $form_state->getValue('api_key'))
->save();
drupal_flush_all_caches();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Drupal\farm_map\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Url;
use Drupal\farm_map\Event\MapRenderEvent;
/**
* Provides a farm_map render element.
*
* @RenderElement("farm_map")
*/
class FarmMap extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#pre_render' => [
[$class, 'preRenderMap'],
],
'#theme' => 'farm_map',
'#map_type' => 'default',
'#map_settings' => [],
'#behaviors' => [],
];
}
/**
* Pre-render callback for the map render array.
*
* @param array $element
* A renderable array containing a #map_type property, which will be
* appended to 'farm-map-' as the map element ID if one has not already
* been provided.
*
* @return array
* A renderable array representing the map.
*
* @see \Drupal\farm_map\Event\MapRenderEvent
*/
public static function preRenderMap(array $element) {
if (empty($element['#attributes']['id'])) {
// Set the id to the map name.
$map_id = Html::getUniqueId('farm-map-' . $element['#map_type']);
$element['#attributes']['id'] = $map_id;
}
else {
$map_id = $element['#attributes']['id'];
}
// Get the entity type manager.
$entity_type_manager = \Drupal::entityTypeManager();
// Get the map type.
/** @var \Drupal\farm_map\Entity\MapTypeInterface $map */
$map = $entity_type_manager->getStorage('map_type')->load($element['#map_type']);
// Add the farm-map class.
$element['#attributes']['class'][] = 'farm-map';
// By default, inform farm_map.js that it should instantiate the map.
if (empty($element['#attributes']['data-map-instantiator'])) {
$element['#attributes']['data-map-instantiator'] = 'farm_map';
}
// Attach the farmOS-map and farm_map libraries.
$element['#attached']['library'][] = 'farm_map/farmOS-map';
$element['#attached']['library'][] = 'farm_map/farm_map';
// Determine the public path for the farmOS-map library.
// Get the farm_map/farmOS-map library.
/** @var \Drupal\Core\Asset\LibraryDiscovery $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
$library_info = $library_discovery->getLibraryByName('farm_map', 'farmOS-map');
// Build an absolute server path to the farmOS-map library that includes the
// Drupal base path.
$js_path = $library_info['js'][0]['data'];
$absolute_js_path = Url::fromUri("base:$js_path")->setAbsolute(FALSE)->toString();
// Remove 13 characters of farmOS-map.js from the path.
$public_path = substr($absolute_js_path, 0, -13);
// Add public base path as settings for farm_map_public_path.
$element['#attached']['drupalSettings']['farm_map_public_path'] = $public_path;
// If #behaviors are included, attach each one.
foreach ($element['#behaviors'] as $behavior_name) {
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $behavior */
$behavior = $entity_type_manager->getStorage('map_behavior')->load($behavior_name);
if (!is_null($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($element['#map_settings'], $map_options);
$element['#attached']['drupalSettings']['farm_map'][$map_id] = $instance_settings;
// Create and dispatch a MapRenderEvent.
$event = new MapRenderEvent($map, $element, $entity_type_manager);
\Drupal::service('event_dispatcher')->dispatch($event, MapRenderEvent::EVENT_NAME);
// Return the element.
return $event->element;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\farm_map\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElementBase;
use Drupal\geofield\GeoPHP\GeoPHPWrapper;
/**
* Form element that returns WKT rendered in a map.
*
* @FormElement("farm_map_input")
*/
class FarmMapInput extends FormElementBase {
/**
* {@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');
if ($display_raw_geometry) {
$element_title .= ' ' . t('WKT');
}
$element['value'] = [
'#type' => $display_raw_geometry ? 'textarea' : 'hidden',
'#title' => $element_title,
'#title_display' => 'invisible',
'#attributes' => [
'data-map-geometry-field' => TRUE,
],
'#disabled' => $element['#disabled'],
'#required' => $element['#required'],
];
// 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;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the LayerStyle config entity.
*
* @ConfigEntityType(
* id = "layer_style",
* label = @Translation("Layer style"),
* label_collection = @Translation("Layer styles"),
* handlers = { },
* admin_permission = "administer farm map",
* entity_keys = {
* "id" = "id",
* },
* config_export = {
* "id",
* "color",
* "conditions",
* },
* )
*
* @ingroup farm
*/
class LayerStyle extends ConfigEntityBase implements LayerStyleInterface {
/**
* The layer style ID.
*
* @var string
*/
protected $id;
/**
* The layer style conditions.
*
* @var mixed|null
*/
protected $conditions;
/**
* {@inheritdoc}
*/
public function getConditions() {
return $this->conditions;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface for defining LayerStyle config entities.
*/
interface LayerStyleInterface extends ConfigEntityInterface {
/**
* Returns the layer style conditions.
*
* @return mixed|null
* The layer style conditions.
*/
public function getConditions();
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the MapBehavior config entity.
*
* @ConfigEntityType(
* id = "map_behavior",
* label = @Translation("Map behavior"),
* label_collection = @Translation("Map behaviors"),
* handlers = { },
* admin_permission = "administer farm map",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* config_export = {
* "id",
* "label",
* "description",
* "library",
* "settings",
* },
* )
*
* @ingroup farm
*/
class MapBehavior extends ConfigEntityBase implements MapBehaviorInterface {
/**
* The map behavior ID.
*
* @var string
*/
protected $id;
/**
* The map behavior label.
*
* @var string
*/
protected $label;
/**
* The behavior library name.
*
* @var string
*/
protected $library;
/**
* A brief description of this map behavior.
*
* @var string
*/
protected $description;
/**
* The behavior settings.
*
* @var mixed|null
*/
protected $settings;
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->label;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
return $this->set('description', $description);
}
/**
* {@inheritdoc}
*/
public function getLibrary() {
return $this->library;
}
/**
* {@inheritdoc}
*/
public function getSettings() {
return $this->settings;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDescriptionInterface;
/**
* Provides an interface for defining MapBehavior config entities.
*/
interface MapBehaviorInterface extends ConfigEntityInterface, EntityDescriptionInterface {
/**
* Returns the behavior library.
*
* @return string
* The behavior library.
*/
public function getLibrary();
/**
* Returns the behavior settings.
*
* @return mixed|null
* The behavior settings.
*/
public function getSettings();
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the MapType config entity.
*
* @ConfigEntityType(
* id = "map_type",
* label = @Translation("Map type"),
* label_collection = @Translation("Map types"),
* handlers = { },
* admin_permission = "administer farm map",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* config_export = {
* "id",
* "label",
* "description",
* "behaviors",
* "options",
* },
* )
*
* @ingroup farm
*/
class MapType extends ConfigEntityBase implements MapTypeInterface {
/**
* The map type ID.
*
* @var string
*/
protected $id;
/**
* The map type label.
*
* @var string
*/
protected $label;
/**
* A brief description of this map type.
*
* @var string
*/
protected $description;
/**
* Behaviors to add to the map.
*
* @var string[]
*/
protected $behaviors;
/**
* The options to pass to farmOS-map.
*
* @var mixed|null
*/
protected $options;
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->label;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
return $this->set('description', $description);
}
/**
* {@inheritdoc}
*/
public function getMapBehaviors() {
return $this->behaviors ?? [];
}
/**
* {@inheritdoc}
*/
public function getMapOptions() {
return $this->options;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\farm_map\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDescriptionInterface;
/**
* Provides an interface for defining MapType config entities.
*/
interface MapTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface {
/**
* Returns behaviors to add to the map.
*
* @return string[]
* The list of map behaviors.
*/
public function getMapBehaviors();
/**
* Returns the options to pass to farmOS-map.
*
* @return mixed|null
* The map options.
*/
public function getMapOptions();
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Drupal\farm_map\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\farm_map\Entity\MapTypeInterface;
/**
* An event that is dispatched before rendering a map on the page.
*
* @group farm
*/
class MapRenderEvent extends Event {
const EVENT_NAME = 'map_render_event';
/**
* The farm_map render element.
*
* @var \Drupal\farm_map\Element\FarmMap
*/
public $element;
/**
* The map type config entity.
*
* @var \Drupal\farm_map\Entity\MapTypeInterface
*/
private $mapType;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* MapRenderEvent constructor.
*
* @param \Drupal\farm_map\Entity\MapTypeInterface $map_type
* The farm_map render element.
* @param array $element
* The farm_map render array.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(MapTypeInterface $map_type, array $element, EntityTypeManagerInterface $entity_type_manager) {
$this->element = $element;
$this->mapType = $map_type;
$this->entityTypeManager = $entity_type_manager;
}
/**
* Getter method to get the map target ID.
*
* @return string
* The map target ID.
*/
public function getMapTargetId() {
return $this->element['#attributes']['id'];
}
/**
* Getter method to get the map type being rendered.
*
* @return \Drupal\farm_map\Entity\MapTypeInterface
* The map type config entity.
*/
public function getMapType() {
return $this->mapType;
}
/**
* Getter method for map behaviors.
*
* This returns a merged list of map behaviors from both the map type
* configuration and the map element's #behaviors property.
*
* @return string[]
* An array of map behavior IDs.
*/
public function getMapBehaviors() {
$behaviors = $this->getMapType()->getMapBehaviors();
if (!empty($this->element['#behaviors'])) {
$behaviors = array_merge($behaviors, $this->element['#behaviors']);
}
return $behaviors;
}
/**
* Add behavior to the map.
*
* @param string $behavior_name
* The behavior name.
* @param array $settings
* Optional behavior settings that will be added to
* drupalSettings.farm_map.behaviors.behavior_name.
*
* @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 = []) {
// Load the behavior.
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $behavior */
$behavior = $this->entityTypeManager->getStorage('map_behavior')->load($behavior_name);
// If the behavior has a library, attach it.
if (!empty($behavior->getLibrary())) {
$this->element['#attached']['library'][] = $behavior->getLibrary();
}
// Add behavior settings if supplied.
if (!empty($settings)) {
$behaviorSettings['behaviors'][$behavior_name] = $settings;
$this->addSettings($behaviorSettings);
}
}
/**
* Add settings to the map.
*
* These settings will be added to drupalSettings.farm_map.
*
* @param array $settings
* The settings to be added.
*/
public function addSettings(array $settings) {
$existing = [];
if (!empty($this->element['#attached']['drupalSettings']['farm_map'])) {
$existing = $this->element['#attached']['drupalSettings']['farm_map'];
}
$this->element['#attached']['drupalSettings']['farm_map'] = array_replace_recursive($existing, $settings);
}
/**
* Add cache tags to the render element.
*
* @param array $tags
* An array of cache tags.
*/
public function addCacheTags(array $tags) {
$existing = [];
if (!empty($this->element['#cache']['tags'])) {
$existing = $this->element['#cache']['tags'];
}
$this->element['#cache']['tags'] = array_unique(array_merge($tags, $existing));
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\farm_map\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\farm_map\Event\MapRenderEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber for the MapRenderEvent.
*
* Adds default behaviors to maps.
*/
class MapRenderEventSubscriber implements EventSubscriberInterface {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
private $configFactory;
/**
* Constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@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) {
// Add the map type cache tags.
$event->addCacheTags($event->getMapType()->getCacheTags());
// Include map behaviors defined by the map type.
$map_behaviors = $event->getMapType()->getMapBehaviors();
foreach ($map_behaviors as $behavior) {
$event->addBehavior($behavior);
}
// Add the WKT behavior if the render element has WKT.
if (!empty($event->element['#map_settings']['wkt'])) {
$event->addBehavior('wkt');
}
// Get whether the side panel should be enabled.
$enable_side_panel = $this->configFactory->get('farm_map.settings')->get('enable_side_panel');
// Set a cache tag on the map settings to invalidate the cache on changes.
$event->addCacheTags(['config:farm_map.settings']);
if ($enable_side_panel) {
$event->addBehavior('enable_side_panel');
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\farm_map\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a farm_map settings form.
*/
class MapSettingsForm extends ConfigFormbase {
/**
* Config settings.
*
* @var string
*/
const SETTINGS = 'farm_map.settings';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'farm_map_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
static::SETTINGS,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateinterface $form_state) {
$config = $this->config(static::SETTINGS);
// Add the enable side panel option.
$form['enable_side_panel'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Side Panel'),
'#description' => $this->t('Enable the side panel in farmOS maps for displaying additional settings and information.'),
'#default_value' => $config->get('enable_side_panel'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->configFactory->getEditable(static::SETTINGS)
->set('enable_side_panel', $form_state->getValue('enable_side_panel'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\farm_map;
use Drupal\farm_map\Entity\LayerStyle;
use Drupal\farm_map\Entity\LayerStyleInterface;
/**
* Layer style loader.
*/
class LayerStyleLoader implements LayerStyleLoaderInterface {
/**
* {@inheritdoc}
*/
public function load(array $conditions = []): ?LayerStyleInterface {
// Load all LayerStyle config entities.
/** @var \Drupal\farm_map\Entity\LayerStyleInterface[] $layer_styles */
$layer_styles = LayerStyle::loadMultiple();
// If there are conditions, filter the styles.
if (!empty($conditions)) {
foreach ($conditions as $key => $value) {
$layer_styles = array_filter($layer_styles, function ($layer_style) use ($key, $value) {
$style_conditions = $layer_style->getConditions();
if (isset($style_conditions[$key])) {
if (is_array($style_conditions[$key]) && in_array($value, $style_conditions[$key])) {
return TRUE;
}
if ($style_conditions[$key] == $value) {
return TRUE;
}
}
return FALSE;
});
}
}
// If the filtered styles are not empty, return the first one.
if (!empty($layer_styles)) {
return reset($layer_styles);
}
return NULL;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\farm_map;
use Drupal\farm_map\Entity\LayerStyleInterface;
/**
* Layer style loader interface.
*/
interface LayerStyleLoaderInterface {
/**
* Load a single layer style definition, optionally filtered by conditions.
*
* @param array $conditions
* An array of conditions for filtering.
*
* @return \Drupal\farm_map\Entity\LayerStyleInterface|null
* Returns a layer style definition, or NULL if no matches were found.
*/
public function load(array $conditions = []): ?LayerStyleInterface;
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Drupal\farm_map\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginDependencyTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a map block.
*
* @Block(
* id = "map_block",
* admin_label = @Translation("Map block"),
* )
*/
class MapBlock extends BlockBase implements ContainerFactoryPluginInterface {
use PluginDependencyTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a MapBlock object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'map_type' => 'default',
'map_behaviors' => [],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Map type.
$map_types = $this->entityTypeManager->getStorage('map_type')->loadMultiple();
$map_type_options = array_map(function ($map_type) {
/** @var \Drupal\farm_map\Entity\MapTypeInterface $map_type */
return $map_type->label();
}, $map_types);
$form['map_type'] = [
'#type' => 'select',
'#title' => $this->t('Map type'),
'#options' => $map_type_options,
'#default_value' => $this->configuration['map_type'],
];
// Map behaviors.
$map_behaviors = $this->entityTypeManager->getStorage('map_behavior')->loadMultiple();
$map_behavior_options = array_map(function ($map_behavior) {
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $map_behavior */
return $map_behavior->label();
}, $map_behaviors);
$form['map_behaviors'] = [
'#type' => 'select',
'#title' => $this->t('Map behaviors'),
'#description' => $this->t('Add additional behaviors to the map. This form lists all available behaviors, but be aware that some behaviors may require additional settings that must be provided by modules and will not work properly on their own. Note that behaviors may also be added to maps automatically by modules, even if they are not selected in this list. Using a custom map type is one way to avoid this.'),
'#options' => $map_behavior_options,
'#default_value' => $this->configuration['map_behaviors'],
'#multiple' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
// Save map config values if no errors occurred.
if (!$form_state->getErrors()) {
$this->configuration['map_type'] = $form_state->getValue('map_type');
$this->configuration['map_behaviors'] = array_keys($form_state->getValue('map_behaviors'));
}
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#type' => 'farm_map',
'#map_type' => $this->configuration['map_type'] ?? 'default',
'#behaviors' => $this->configuration['map_behaviors'] ?? [],
];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// Add map type dependencies.
/** @var \Drupal\farm_map\Entity\MapTypeInterface $map_type */
$map_type = $this->entityTypeManager->getStorage('map_type')->load($this->configuration['map_type'] ?? 'default');
$this->addDependencies($map_type->getDependencies());
// Add map behavior dependencies.
$map_behaviors = $this->configuration['map_behaviors'] ?? [];
if (!empty($map_behaviors)) {
/** @var \Drupal\farm_map\Entity\MapBehaviorInterface $behavior */
foreach ($this->entityTypeManager->getStorage('map_behavior')->loadMultiple($map_behaviors) as $behavior) {
$this->addDependencies($behavior->getDependencies());
}
}
return $this->dependencies;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Drupal\farm_map\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\geofield\GeoPHP\GeoPHPInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the map 'geofield' formatter.
*
* @FieldFormatter(
* id = "farm_map_geofield",
* label = @Translation("farmOS Map"),
* field_types = {
* "geofield"
* }
* )
*/
class GeofieldFormatter extends FormatterBase {
/**
* The geofield.geophp service.
*
* @var \Drupal\geofield\GeoPHP\GeoPHPInterface
*/
protected $geoPhp;
/**
* Constructs a FormatterBase object.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\geofield\GeoPHP\GeoPHPInterface $geo_php
* The geofield geophp service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, GeoPHPInterface $geo_php) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->geoPhp = $geo_php;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('geofield.geophp'),
);
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
// Build a render element.
$element = [];
// First check to see if we have any value and remove any unset deltas.
foreach ($items as $delta => $item) {
if (empty($item->get('value')->getValue())) {
unset($items[$delta]);
}
}
// If there are no items, stop here. We won't show anything.
if ($items->isEmpty()) {
return $element;
}
// Create array of features.
$features = [];
foreach ($items as $delta) {
// Get the field value.
$value = $delta->get('value')->getValue();
// Convert to WKT.
$geom = $this->geoPhp->load($value);
$features[] = $geom->out('wkt');
}
// If there are no features at this point, bail.
if (empty($features)) {
return $element;
}
// Build a map for each item.
foreach ($features as $delta => $feature) {
$element[$delta] = [
'#type' => 'farm_map',
'#map_type' => 'geofield',
'#map_settings' => [
'wkt' => $feature,
'behaviors' => [
'wkt' => [
'zoom' => TRUE,
],
],
],
];
}
return $element;
}
}

View File

@@ -0,0 +1,350 @@
<?php
namespace Drupal\farm_map\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\File\FileSystem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\farm_geo\Traits\WktTrait;
use Drupal\file\FileInterface;
use Drupal\geofield\GeoPHP\GeoPHPInterface;
use Drupal\geofield\Plugin\Field\FieldWidget\GeofieldBaseWidget;
use Drupal\geofield\Plugin\GeofieldBackendManager;
use Drupal\geofield\WktGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the map 'geofield' widget.
*
* @FieldWidget(
* id = "farm_map_geofield",
* label = @Translation("farmOS Map"),
* field_types = {
* "geofield"
* }
* )
*/
class GeofieldWidget extends GeofieldBaseWidget {
use WktTrait;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Supported GeoPHP file types.
*
* @var string[]
* GeoPHP type keyed by file extension.
*/
public static $geoPhpTypes = [
'geojson' => 'geojson',
'gpx' => 'gpx',
'kml' => 'kml',
'kmz' => 'kml',
'wkb' => 'wkb',
'wkt' => 'wkt',
];
/**
* GeofieldWidget constructor.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param array $third_party_settings
* Any third party settings settings.
* @param \Drupal\geofield\GeoPHP\GeoPHPInterface $geophp_wrapper
* The geoPhpWrapper.
* @param \Drupal\geofield\WktGeneratorInterface $wkt_generator
* The WKT format Generator service.
* @param \Drupal\geofield\Plugin\GeofieldBackendManager $geofield_backend_manager
* The geofieldBackendManager.
* @param \Drupal\Core\File\FileSystem $file_system
* The file system service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, GeoPHPInterface $geophp_wrapper, WktGeneratorInterface $wkt_generator, GeofieldBackendManager $geofield_backend_manager, FileSystem $file_system, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $geophp_wrapper, $wkt_generator, $geofield_backend_manager);
$this->fileSystem = $file_system;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('geofield.geophp'),
$container->get('geofield.wkt_generator'),
$container->get('plugin.manager.geofield_backend'),
$container->get('file_system'),
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'display_raw_geometry' => TRUE,
'populate_file_field' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$elements['display_raw_geometry'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display raw geometry'),
'#default_value' => $this->getSetting('display_raw_geometry'),
];
$elements['populate_file_field'] = [
'#type' => 'textfield',
'#title' => $this->t('File field to populate geometry from.'),
'#default_value' => $this->getSetting('populate_file_field'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
// 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['#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]);
$field_value = $items[$delta]->value;
$current_value = $form_value['value'] ?? $field_value;
$element['#default_value'] = $current_value;
// Configure to display raw geometry.
$display_raw_geometry = $this->getSetting('display_raw_geometry');
$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
// field must be included in the current form.
$populate_file_field = $this->getSetting('populate_file_field');
if (!empty($populate_file_field) && !empty($form[$populate_file_field])) {
$element['trigger'] = [
'#type' => 'submit',
'#value' => $this->t('Import geometry from uploaded files'),
'#submit' => [[$this, 'fileParse']],
'#ajax' => [
'wrapper' => $field_wrapper_id,
'callback' => [$this, 'fileCallback'],
'message' => $this->t('Working...'),
],
'#states' => [
'disabled' => [
':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;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as $delta => $value) {
$values[$delta]['value'] = $this->geofieldBackendValue($value['value']);
}
return $values;
}
/**
* Submit function to parse geometries from uploaded files.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function fileParse(array &$form, FormStateInterface $form_state) {
// Bail if no populate file field is not configured.
$populate_file_field = $this->getSetting('populate_file_field');
if (empty($populate_file_field)) {
return;
}
// Get the form field element.
$triggering_element = $form_state->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
// Load the uploaded files.
$uploaded_files = $form_state->getValue($populate_file_field);
if (!empty($uploaded_files)) {
// Get file IDs.
$file_ids = array_reduce($uploaded_files, function ($carry, $file) {
return array_merge($carry, array_values($file['fids']));
}, []);
// Load and process each file.
/** @var \Drupal\file\Entity\File[] $files */
$files = $this->entityTypeManager->getStorage('file')->loadMultiple($file_ids);
// @todo Support geometry field with > 1 cardinality.
$wkt_strings = [];
if (!empty($files)) {
foreach ($files as $file) {
// Get the geometry type.
$geophp_type = $this->getGeoPhpType($file);
// Bail if the file is not a supported format.
if ($geophp_type === FALSE) {
$this->messenger()->addWarning(
$this->t('%filename is not a supported geometry file format. Supported formats: %formats',
['%filename' => $file->getFilename(), '%formats' => implode(', ', array_keys(static::$geoPhpTypes))]
));
return;
}
// Try to parse geometry using the specified geoPHP type.
$path = $file->getFileUri();
if ($geophp_type == 'kml' && $file->getMimeType() === 'application/vnd.google-earth.kmz' && extension_loaded('zip')) {
$path = 'zip://' . $this->fileSystem->realpath($path) . '#doc.kml';
}
$data = file_get_contents($path);
if ($geom = $this->geoPhpWrapper->load($data, $geophp_type)) {
$wkt_strings[] = $geom->out('wkt');
}
}
}
// Merge WKT geometries into a single geometry collection.
$wkt = '';
if (!empty($wkt_strings)) {
if (count($wkt_strings) > 1) {
$wkt = $this->combineWkt($wkt_strings);
}
else {
$wkt = reset($wkt_strings);
}
}
// Bail if no geometry was parsed.
if (empty($wkt)) {
$this->messenger()->addWarning($this->t('No geometry could be parsed from files.'));
return;
}
// Unset the current geometry value from the user input.
$field_name = $this->fieldDefinition->getName();
$delta = $element['#delta'];
$user_input = $form_state->getUserInput();
unset($user_input[$field_name][$delta]);
$form_state->setUserInput($user_input);
// Set the new form value.
$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);
}
}
/**
* AJAX callback for the find using files field button.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array|mixed|null
* The map form element to replace
*/
public function fileCallback(array &$form, FormStateInterface $form_state) {
// Return the rebuilt map form field field element.
$triggering_element = $form_state->getTriggeringElement();
return NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
}
/**
* Helper function to check if the file extension is a supported geometry.
*
* @param \Drupal\file\FileInterface $file
* The file to check.
*
* @return string|false
* The GeoPHP type or FALSE.
*/
private function getGeoPhpType(FileInterface $file) {
// Get the file extension.
$matches = [];
if (preg_match('/(?<=\.)[^.]+$/', $file->getFilename(), $matches) && isset($matches[0])) {
// Return the associated GeoPHP type.
if (isset(self::$geoPhpTypes[$matches[0]])) {
return self::$geoPhpTypes[$matches[0]];
}
}
// Otherwise the file extension is not a valid GeoPHP geometry type.
return FALSE;
}
}

View File

@@ -0,0 +1,12 @@
{#
/**
* @file
* Default theme implementation for the farm_map render element.
*
* Available variables:
* - attributes: Attributes for the farm_map wrapper div.
*
* @ingroup themeable
*/
#}
<div{{ attributes }}></div>

View File

@@ -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

View File

@@ -0,0 +1,10 @@
name: farmOS map tests
type: module
description: Support module for farmOS map testing.
package: Testing
core_version_requirement: ^10
dependencies:
- farm:farm_location
- farm:farm_map
- farm:farm_quick
- log:log

View File

@@ -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'),
]);
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Drupal\Tests\farm_map\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Tests\field\Functional\FieldTestBase;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the farmOS Geofield widget.
*
* @group farm
*/
class GeofieldWidgetTest extends FieldTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_map',
'geofield',
'entity_test',
];
/**
* {@inheritdoc}
*/
protected $profile = 'farm';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A field storage with cardinality 1 to use in this test class.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* A Field to use in this test class.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fieldStorage = FieldStorageConfig::create([
'field_name' => 'geofield_field',
'entity_type' => 'entity_test',
'type' => 'geofield',
'settings' => [
'backend' => 'geofield_backend_default',
],
]);
$this->fieldStorage->save();
$this->field = FieldConfig::create([
'field_storage' => $this->fieldStorage,
'bundle' => 'entity_test',
'settings' => [
'backend' => 'geofield_backend_default',
],
]);
$this->field->save();
// Create a web user.
$this->drupalLogin($this->drupalCreateUser(['view test entity', 'administer entity_test content']));
}
/**
* Test the farmOS Geofield widget.
*/
public function testGeofieldWidget() {
EntityFormDisplay::load('entity_test.entity_test.default')
->setComponent($this->fieldStorage->getName(), [
'type' => 'farm_map_geofield',
])
->save();
// Create an entity.
$entity = EntityTest::create([
'user_id' => 1,
'name' => $this->randomMachineName(),
]);
$entity->save();
// With no field data, no buttons are checked.
$this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
$this->assertSession()->pageTextContains('geofield_field');
// Test a valid WKT value.
$edit = [
'name[0][value]' => 'Arnedo',
'geofield_field[0][value]' => 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))',
];
$this->submitForm($edit, 'Save');
$this->assertFieldValues($entity, 'geofield_field', ['POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))']);
// Test a valid GeoJSON value.
$edit = [
'name[0][value]' => 'Dinagat Islands',
'geofield_field[0][value]' => '{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [125.6, 10.1]
},
"properties": {
"name": "Dinagat Islands"
}
}',
];
$this->submitForm($edit, 'Save');
$this->assertFieldValues($entity, 'geofield_field', ['POINT (125.6 10.1)']);
// Test a valid WKB value.
$edit = [
'name[0][value]' => 'Arnedo',
'geofield_field[0][value]' => '0101000020E6100000705F07CE19D100C0865AD3BCE31C4540',
];
$this->submitForm($edit, 'Save');
$this->assertFieldValues($entity, 'geofield_field', ['POINT (-2.1021 42.2257)']);
}
}

View File

@@ -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.'));
}
}