Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 40 additions & 25 deletions GeolocationPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function hookInstall()
`zoom_level` INT NOT NULL ,
`address` TEXT NOT NULL ,
`label` VARCHAR( 255 ) NOT NULL DEFAULT '' ,
`geometry_json` TEXT NOT NULL ,
INDEX (`item_id`)) ENGINE = InnoDB";
$db->query($sql);

Expand All @@ -71,6 +72,7 @@ public function hookInstall()
set_option('geolocation_basemap', self::DEFAULT_BASEMAP);
set_option('geolocation_geocoder', self::DEFAULT_GEOCODER);
set_option('geolocation_item_map_enable', '1');
set_option('geolocation_auto_fit_browse', '1');
}

public function hookUninstall()
Expand Down Expand Up @@ -163,7 +165,13 @@ public function hookUpgrade($args)
}
if (version_compare($args['old_version'], '4.0', '<')) {
$db = get_db();
$db->query("ALTER TABLE `$db->Location` ADD COLUMN `label` VARCHAR(255) NOT NULL DEFAULT '' AFTER `address`, DROP COLUMN `map_type`");
// Three steps: add nullable, back-fill from existing lat/lng,
// then tighten to NOT NULL. MySQL rejects adding a NOT NULL
// column to a non-empty table without a default, and a
// placeholder default would corrupt the existing coordinate data.
$db->query("ALTER TABLE `$db->Location` ADD COLUMN `label` VARCHAR(255) NOT NULL DEFAULT '' AFTER `address`, DROP COLUMN `map_type`, ADD COLUMN `geometry_json` TEXT NULL");
$db->query("UPDATE `$db->Location` SET `geometry_json` = CONCAT('{\"type\":\"Point\",\"coordinates\":[', `longitude`, ',', `latitude`, ']}')");
$db->query("ALTER TABLE `$db->Location` MODIFY COLUMN `geometry_json` TEXT NOT NULL");
}
}

Expand Down Expand Up @@ -263,13 +271,17 @@ private function _head()
$version = Zend_Registry::get('plugin_loader')->getPlugin('Geolocation')->getIniVersion();
queue_css_file('leaflet/leaflet', null, null, 'javascripts', $version);
queue_css_file('leaflet-draw/leaflet.draw', null, null, 'javascripts', $version);
queue_css_file('geolocation-marker', null, null, 'css', $version);
queue_js_file(['leaflet/leaflet', 'leaflet/leaflet-providers', 'leaflet-draw/leaflet.draw', 'map'], 'javascripts', [], $version);
queue_css_file('geolocation-map', null, null, 'css', $version);

// Marker clustering is optional, so its assets load only when enabled.
// leaflet-deflate always loads because every map initializes it.
$jsFiles = ['leaflet/leaflet', 'leaflet/leaflet-providers', 'leaflet-draw/leaflet.draw', 'leaflet-deflate/L.Deflate'];
if (get_option('geolocation_cluster')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the switch to always load the clustering support?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed that override so the item-show map follows the geolocation_cluster option like the browse map, and reverted _head() to load markercluster only when clustering is enabled.

queue_css_file(['MarkerCluster', 'MarkerCluster.Default'], null, null, 'javascripts/leaflet-markercluster', $version);
queue_js_file('leaflet-markercluster/leaflet.markercluster', 'javascripts', [], $version);
$jsFiles[] = 'leaflet-markercluster/leaflet.markercluster';
}
$jsFiles[] = 'map';
queue_js_file($jsFiles, 'javascripts', [], $version);
}

public function hookAfterSaveItem($args)
Expand All @@ -281,7 +293,7 @@ public function hookAfterSaveItem($args)
$item = $args['record'];
// geolocation_form_shown is a sentinel set by input-partial.php. Its
// presence means the map form was rendered, so an empty geolocation_locations
// value means all markers were deleted, not that the form was absent.
// value means all locations were deleted, not that the form was absent.
if (!isset($post['geolocation_form_shown'])) {
return;
}
Expand All @@ -295,7 +307,7 @@ public function hookAfterSaveItem($args)
}

foreach (json_decode($post['geolocation_locations'] ?? '[]', true) as $entry) {
if (!is_numeric($entry['latitude'] ?? null) || !is_numeric($entry['longitude'] ?? null)) {
if (empty($entry['geometry_json'])) {
continue;
}
$id = !empty($entry['id']) ? (int) $entry['id'] : null;
Expand Down Expand Up @@ -648,9 +660,9 @@ public function geolocationShortcode($args)
$options = [];

if (isset($args['fit'])) {
$options['fitMarkers'] = $booleanFilter->filter($args['fit']);
$options['fitLocations'] = $booleanFilter->filter($args['fit']);
} else {
$options['fitMarkers'] = '1';
$options['fitLocations'] = '1';
}

if (isset($args['type'])) {
Expand Down Expand Up @@ -700,27 +712,29 @@ protected function _mapForm($item, $label = '', $view = null)
$existingLocations = [];
if (isset($_POST['geolocation_form_shown'])) {
foreach (json_decode($_POST['geolocation_locations'] ?? '[]', true) as $entry) {
if (!is_numeric($entry['latitude'] ?? null) || !is_numeric($entry['longitude'] ?? null)) {
if (empty($entry['geometry_json'])) {
continue;
}
$existingLocations[] = [
'id' => !empty($entry['id']) ? (int) $entry['id'] : null,
'latitude' => (float) $entry['latitude'],
'longitude' => (float) $entry['longitude'],
'zoom_level' => (int) ($entry['zoom_level'] ?? 0),
'address' => $entry['address'] ?? '',
'label' => $entry['label'] ?? '',
'id' => !empty($entry['id']) ? (int) $entry['id'] : null,
'latitude' => (float) ($entry['latitude'] ?? 0),
'longitude' => (float) ($entry['longitude'] ?? 0),
'zoom_level' => (int) ($entry['zoom_level'] ?? 0),
'address' => $entry['address'] ?? '',
'label' => $entry['label'] ?? '',
'geometry_json' => $entry['geometry_json'],
];
}
} elseif ($item && $item->id) {
foreach ($this->_db->getTable('Location')->findBy(['item_id' => $item->id]) as $loc) {
$existingLocations[] = [
'id' => $loc->id,
'latitude' => $loc->latitude,
'longitude' => $loc->longitude,
'zoom_level' => $loc->zoom_level,
'address' => $loc->address,
'label' => $loc->label,
'id' => $loc->id,
'latitude' => $loc->latitude,
'longitude' => $loc->longitude,
'zoom_level' => $loc->zoom_level,
'address' => $loc->address,
'label' => $loc->label,
'geometry_json' => $loc->geometry_json,
];
}
}
Expand Down Expand Up @@ -803,7 +817,7 @@ public function filterStaticSiteExportOmekaShortcodeCallbacks($callbacks)
// @see GeolocationPlugin::geolocationShortcode()
$callbacks['geolocation'] = function ($args, $frontMatter, $job) {
$frontMatter['css'][] = 'vendor/leaflet/leaflet.css';
$frontMatter['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
$frontMatter['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatter['js'][] = 'vendor/jquery/jquery.js';
$frontMatter['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatter['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
Expand All @@ -825,7 +839,7 @@ public function hookStaticSiteExportSiteExportPost($args)
'title' => __('Map'),
'css' => [
'vendor/leaflet/leaflet.css',
'vendor/omeka-geolocation/geolocation-marker.css',
'vendor/omeka-geolocation/geolocation-map.css',
],
'js' => [
'vendor/jquery/jquery.js',
Expand Down Expand Up @@ -869,7 +883,7 @@ public function hookStaticSiteExportItemBundle($args)
}

$frontMatterPage['css'][] = 'vendor/leaflet/leaflet.css';
$frontMatterPage['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
$frontMatterPage['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatterPage['js'][] = 'vendor/jquery/jquery.js';
$frontMatterPage['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatterPage['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
Expand Down Expand Up @@ -916,7 +930,7 @@ public function hookExhibitBuilderStaticSiteExportExhibitPageBlock($args)
$attachments = $exhibitPageBlock->getAttachments();

$frontMatterExhibitPage['css'][] = 'vendor/leaflet/leaflet.css';
$frontMatterExhibitPage['css'][] = 'vendor/omeka-geolocation/geolocation-marker.css';
$frontMatterExhibitPage['css'][] = 'vendor/omeka-geolocation/geolocation-map.css';
$frontMatterExhibitPage['js'][] = 'vendor/jquery/jquery.js';
$frontMatterExhibitPage['js'][] = 'vendor/leaflet/leaflet.js';
$frontMatterExhibitPage['js'][] = 'vendor/omeka-geolocation/geolocation-locations.js';
Expand Down Expand Up @@ -945,6 +959,7 @@ private function _locationToStaticSiteExportArray(Location $location, Item $item
{
$file = $item->getFile();
return [
'geometry_json' => $location->geometry_json,
'latitude' => $location->latitude,
'longitude' => $location->longitude,
'zoomLevel' => $location->zoom_level,
Expand Down
4 changes: 2 additions & 2 deletions config_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@
</div>
<div class="field">
<div class="two columns alpha">
<label for="cluster"><?php echo __('Enable marker clustering'); ?></label>
<label for="cluster"><?php echo __('Enable location clustering'); ?></label>
</div>
<div class="inputs five columns omega">
<p class="explanation"><?php echo __('Show close or overlapping markers as clusters.'); ?></p>
<p class="explanation"><?php echo __('Show close or overlapping locations as clusters.'); ?></p>
<?php echo $view->formCheckbox('cluster', true, ['checked' => (bool) get_option('geolocation_cluster')]); ?>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
const featureGroup = L.featureGroup();

// Get the locations data and add the locations to the map.
let lastGeometry = null;
locationsData.forEach((locationData) => {
const popupDiv = document.createElement('div');
const popupHeading = document.createElement('h2');
Expand All @@ -27,14 +28,14 @@ document.addEventListener('DOMContentLoaded', function(event) {
popupDiv.appendChild(popupImg);
}

const marker = L.marker([locationData.latitude, locationData.longitude]);
marker.bindPopup(popupDiv);
marker.addTo(featureGroup);
lastGeometry = JSON.parse(locationData.geometry_json);
const layer = L.geoJSON(lastGeometry);
layer.bindPopup(popupDiv);
layer.addTo(featureGroup);
});

map.fitBounds(featureGroup.getBounds());
if (locationsData.length === 1) {
// Set the zoom level if there is only one location.
if (locationsData.length === 1 && lastGeometry.type === 'Point') {
map.setZoom(locationsData[0].zoomLevel ?? 15);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,33 @@ div#geolocation {
padding:0;
}

.geolocation_balloon {
.leaflet-popup-content-wrapper:has(.geolocation-popup) {
overflow: hidden;
padding: 0;
}

.leaflet-popup-content:has(.geolocation-popup) {
margin: 0;
}

.geolocation-popup {
width: 200px;
padding: 0 20px 13px;
}
.geolocation_balloon img {
max-width: 100%;

.geolocation-popup-header {
margin: 0 -20px 13px;
padding: 8px 20px;
background: #e3e3e3;
font-weight: bold;
}

.geolocation-popup a {
border-bottom: none;
}
.geolocation_balloon_title {
font-weight:bold;
font-size:18px;
margin-bottom:0px;

.geolocation-popup img {
max-width: 100%;
}

img.leaflet-tile,
Expand Down
14 changes: 9 additions & 5 deletions models/Api/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function getRepresentation(Omeka_Record_AbstractRecord $record)
$representation = [
'id' => $record->id,
'url' => $this->getResourceUrl("/geolocations/{$record->id}"),
'geometry_json' => $record->geometry_json,
'latitude' => $record->latitude,
'longitude' => $record->longitude,
'zoom_level' => $record->zoom_level,
Expand Down Expand Up @@ -63,11 +64,14 @@ public function setPutData(Omeka_Record_AbstractRecord $record, $data)

private function _applyLocationFields(Omeka_Record_AbstractRecord $record, $data)
{
if (isset($data->latitude)) {
$record->latitude = $data->latitude;
}
if (isset($data->longitude)) {
$record->longitude = $data->longitude;
if (isset($data->geometry_json)) {
$record->geometry_json = $data->geometry_json;
} elseif (isset($data->latitude) && isset($data->longitude)) {
// Fallback for pre-4.0 API clients that post lat/lng without geometry_json
$record->geometry_json = json_encode([
'type' => 'Point',
'coordinates' => [(float) $data->longitude, (float) $data->latitude],
]);
}
if (isset($data->zoom_level)) {
$record->zoom_level = $data->zoom_level;
Expand Down
65 changes: 59 additions & 6 deletions models/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Location extends Omeka_Record_AbstractRecord implements Zend_Acl_Resource_
public $zoom_level;
public $address;
public $label;
public $geometry_json;

/**
* Executes before the record is saved.
Expand All @@ -24,6 +25,26 @@ protected function beforeSave($args)
if (is_null($this->label)) {
$this->label = '';
}
// latitude and longitude are kept in sync with geometry_json so that
// geographic radius search (hookItemsBrowseSql) works for all location
// types without spatial SQL functions. For shapes, we use the bounding
// box center as a representative point.
$geometry = json_decode($this->geometry_json, true);
if ($geometry) {
if ($geometry['type'] === 'Point') {
$this->longitude = $geometry['coordinates'][0];
$this->latitude = $geometry['coordinates'][1];
} else {
// Polygon coordinates[0] is the outer boundary; LineString coordinates is the points array directly
$coords = $geometry['type'] === 'Polygon'
? $geometry['coordinates'][0]
: $geometry['coordinates'];
$lngs = array_column($coords, 0);
$lats = array_column($coords, 1);
$this->longitude = (min($lngs) + max($lngs)) / 2;
$this->latitude = (min($lats) + max($lats)) / 2;
}
}
}

/**
Expand All @@ -38,15 +59,47 @@ protected function _validate()
if (!$this->getTable('Item')->exists($this->item_id)) {
$this->addError('item_id', __('Location requires a valid item ID.'));
}
if (!is_numeric($this->latitude)) {
$this->addError('latitude', __('Location requires a latitude.'));
if (!$this->_isValidGeometry(json_decode($this->geometry_json, true))) {
$this->addError('geometry_json', __('Location requires a valid geometry.'));
}
}

/** Validates that $geometry is a well-formed GeoJSON geometry object. */
private function _isValidGeometry($geometry)
{
if (!is_array($geometry)) {
return false;
}
$type = $geometry['type'] ?? '';
$coords = $geometry['coordinates'] ?? null;
if (!is_array($coords)) {
return false;
}
if ($type === 'Point') {
return $this->_isValidPosition($coords);
}
if (!is_numeric($this->longitude)) {
$this->addError('longitude', __('Location requires a longitude.'));
if ($type === 'LineString') {
return count($coords) >= 2 && $this->_areValidPositions($coords);
}
if (!is_numeric($this->zoom_level)) {
$this->addError('zoom_level', __('Location requires a zoom level.'));
if ($type === 'Polygon') {
return isset($coords[0]) && count($coords[0]) >= 4 && $this->_areValidPositions($coords[0]);
}
return false; // unrecognized type
}

private function _isValidPosition($pos)
{
return is_array($pos) && count($pos) >= 2 && is_numeric($pos[0]) && is_numeric($pos[1]);
}

private function _areValidPositions($positions)
{
foreach ($positions as $pos) {
if (!$this->_isValidPosition($pos)) {
return false;
}
}
return true;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions views/helpers/GeolocationMapBrowse.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public function geolocationMapBrowse($divId = 'map', $options = [], $attrs = [],
$options['uri'] = url('geolocation/map/browse-json');
}

if (!array_key_exists('fitMarkers', $options)) {
$options['fitMarkers'] = (bool) get_option('geolocation_auto_fit_browse');
if (!array_key_exists('fitLocations', $options)) {
$options['fitLocations'] = (bool) get_option('geolocation_auto_fit_browse');
}

$class = 'map geolocation-map';
Expand Down
8 changes: 6 additions & 2 deletions views/helpers/GeolocationMapOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ public function geolocationMapOptions($options = [])
$options['custom_map'] = json_decode((string) get_option('geolocation_custom_map'), true);

$options['strings'] = [
'fitAllMarkers' => __('Fit all markers'),
'label' => __('Label'),
'fitAllLocations' => __('Fit all locations'),
'label' => __('Label'),
'editLocations' => __('Edit locations'),
'noLocationsToEdit' => __('No locations to edit'),
'deleteLocations' => __('Delete locations'),
'noLocationsToDelete' => __('No locations to delete'),
];

return js_escape($options);
Expand Down
Loading