Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0dac7fc
Add support for multiple locations per item
jimsafley Apr 29, 2026
b01aa54
Restore item edit map center from existing location
jimsafley Apr 29, 2026
24c1671
Restore map pan to geocoded location on Find
jimsafley Apr 29, 2026
28cbfb0
Fix geolocations extended resource in items API
jimsafley Apr 29, 2026
2d4ee3a
Remove redundant applySearchFilters override
jimsafley Apr 29, 2026
534a725
Load Leaflet.draw alongside Leaflet on all pages
jimsafley Apr 29, 2026
d60d311
Add fit-all Leaflet control to all map types
jimsafley Apr 29, 2026
8aedd9b
Refine fit-all control: move to module scope, add cleanup, blur on click
jimsafley Apr 29, 2026
619aaa1
Bump version to 4.0 for breaking API and method changes
jimsafley Apr 29, 2026
49e375a
Drop map_type, a holdover from when multiple map providers were suppo…
jimsafley Apr 29, 2026
f34ed50
Add comments to clarify non-obvious code in branch changes
jimsafley Apr 29, 2026
8c1b1b8
Replace KML browse map pipeline with JSON
jimsafley Apr 30, 2026
7b0ff57
Remove center.show, a pre-multi-location holdover
jimsafley Apr 30, 2026
811a010
Use [] destructuring instead of list()
jimsafley May 2, 2026
c060cd3
Remove unnecessary that alias in initMap
jimsafley May 2, 2026
c9332f4
Remove blur() from fit-all control click handler
jimsafley May 2, 2026
bc458b0
Add label as marker title/alt on single item map
jimsafley May 2, 2026
8129bc5
Move browse map marker popup construction from PHP to JavaScript
jimsafley May 2, 2026
c198c0e
Pass existing locations as JSON data attribute instead of inline JS
jimsafley May 2, 2026
da0ced6
Combine ALTER TABLE operations into a single statement
jimsafley May 2, 2026
2c6a3de
Remove single-use _getVersion helper
jimsafley May 2, 2026
0a52241
Submit locations as a single JSON field instead of indexed hidden inputs
jimsafley May 2, 2026
bab2045
Remove redundant empty() check on findBy() result
jimsafley May 2, 2026
ee93c0a
Store location data on Leaflet layers, serialize on submit
jimsafley May 6, 2026
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
278 changes: 121 additions & 157 deletions GeolocationPlugin.php

Large diffs are not rendered by default.

44 changes: 24 additions & 20 deletions controllers/MapController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,34 @@ public function init()

public function browseAction()
{
$table = $this->_helper->db->getTable();
$locationTable = $this->_helper->db->getTable('Location');
[$params, $limit, $currentPage] = $this->_getBrowseParams();

$this->view->totalItems = $this->_helper->db->getTable()->count($params);
$this->view->params = $params;

Zend_Registry::set('pagination', [
'page' => $currentPage,
'per_page' => $limit,
'total_results' => $this->view->totalItems,
]);
}

public function browseJsonAction()
{
[$params, $limit, $currentPage] = $this->_getBrowseParams();

$items = $this->_helper->db->getTable()->findBy($params, $limit, $currentPage);
$this->view->items = $items;
$this->view->locations = $this->_helper->db->getTable('Location')->findLocationsByItem($items);
$this->getResponse()->setHeader('Content-Type', 'application/json');
}

private function _getBrowseParams()
{
$params = $this->getAllParams();
$params['geolocation-mapped'] = true;
$limit = (int) get_option('geolocation_per_page');
$currentPage = $this->getParam('page', 1);

// Only get pagination data for the "normal" page, only get
// item/location data for the KML output.
if ($this->_helper->contextSwitch->getCurrentContext() == 'kml') {
$items = $table->findBy($params, $limit, $currentPage);
$this->view->items = $items;
$this->view->locations = $locationTable->findLocationByItem($items);
} else {
$this->view->totalItems = $table->count($params);
$this->view->params = $params;

$pagination = [
'page' => $currentPage,
'per_page' => $limit,
'total_results' => $this->view->totalItems,
];
Zend_Registry::set('pagination', $pagination);
}
return [$params, $limit, $currentPage];
}
}
37 changes: 12 additions & 25 deletions models/Api/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public function getRepresentation(Omeka_Record_AbstractRecord $record)
'latitude' => $record->latitude,
'longitude' => $record->longitude,
'zoom_level' => $record->zoom_level,
'map_type' => $record->map_type,
'address' => $record->address,
'label' => $record->label,
'item' => [
'id' => $record->item_id,
'url' => $this->getResourceUrl("/items/{$record->item_id}"),
Expand All @@ -47,25 +47,7 @@ public function setPostData(Omeka_Record_AbstractRecord $record, $data)
if (isset($data->item->id)) {
$record->item_id = $data->item->id;
}
if (isset($data->latitude)) {
$record->latitude = $data->latitude;
}
if (isset($data->longitude)) {
$record->longitude = $data->longitude;
}
if (isset($data->zoom_level)) {
$record->zoom_level = $data->zoom_level;
}
if (isset($data->map_type)) {
$record->map_type = $data->map_type;
} else {
$record->map_type = '';
}
if (isset($data->address)) {
$record->address = $data->address;
} else {
$record->address = '';
}
$this->_applyLocationFields($record, $data);
}

/**
Expand All @@ -75,6 +57,11 @@ public function setPostData(Omeka_Record_AbstractRecord $record, $data)
* @param mixed $data
*/
public function setPutData(Omeka_Record_AbstractRecord $record, $data)
{
$this->_applyLocationFields($record, $data);
}

private function _applyLocationFields(Omeka_Record_AbstractRecord $record, $data)
Comment thread
jimsafley marked this conversation as resolved.
{
if (isset($data->latitude)) {
$record->latitude = $data->latitude;
Expand All @@ -85,15 +72,15 @@ public function setPutData(Omeka_Record_AbstractRecord $record, $data)
if (isset($data->zoom_level)) {
$record->zoom_level = $data->zoom_level;
}
if (isset($data->map_type)) {
$record->map_type = $data->map_type;
} else {
$record->map_type = '';
}
if (isset($data->address)) {
$record->address = $data->address;
} else {
$record->address = '';
}
if (isset($data->label)) {
$record->label = $data->label;
} else {
$record->label = '';
}
}
}
19 changes: 7 additions & 12 deletions models/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ class Location extends Omeka_Record_AbstractRecord implements Zend_Acl_Resource_
public $latitude;
public $longitude;
public $zoom_level;
public $map_type;
public $address;
public $label;

/**
* Executes before the record is saved.
*/
protected function beforeSave($args)
{
if (is_null($this->map_type)) {
$this->map_type = '';
}
if (is_null($this->address)) {
$this->address = '';
}
if (is_null($this->label)) {
$this->label = '';
}
}

/**
Expand All @@ -38,18 +38,13 @@ protected function _validate()
if (!$this->getTable('Item')->exists($this->item_id)) {
$this->addError('item_id', __('Location requires a valid item ID.'));
}
// An item can only have one location. This assumes that updating an
// existing location will never modify the item ID.
if (!$this->exists() && $this->getTable()->findBy(['item_id' => $this->item_id])) {
$this->addError('latitude', __('A location already exists for the provided item.'));
}
if (empty($this->latitude)) {
if (!is_numeric($this->latitude)) {
$this->addError('latitude', __('Location requires a latitude.'));
}
if (empty($this->longitude)) {
if (!is_numeric($this->longitude)) {
$this->addError('longitude', __('Location requires a longitude.'));
}
if (empty($this->zoom_level)) {
if (!is_numeric($this->zoom_level)) {
$this->addError('zoom_level', __('Location requires a zoom level.'));
}
}
Expand Down
42 changes: 15 additions & 27 deletions models/Table/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
class Table_Location extends Omeka_Db_Table
{
/**
* Returns a location (or array of locations) for an item (or array of items)
* @param array|Item|int $item An item or item id, or an array of items or item ids
* @param boolean $findOnlyOne Whether or not to return only one location if it exists for the item
* @return array|Location A location or an array of locations
**/
public function findLocationByItem($item, $findOnlyOne = false)
* Returns all locations for an item or array of items, grouped by item_id.
*
* @param array|Item|int $item
* @return array item_id => Location[]
*/
public function findLocationsByItem($item)
{
$db = get_db();

Expand All @@ -16,11 +16,10 @@ public function findLocationByItem($item, $findOnlyOne = false)
} elseif (is_array($item) && !count($item)) {
return [];
}

$alias = $this->getTableAlias();
// Create a SELECT statement for the Location table
$select = $db->select()->from([$alias => $db->Location], "$alias.*");

// Create a WHERE condition that will pull down all the location info
if (is_array($item)) {
$itemIds = [];
foreach ($item as $it) {
Expand All @@ -32,31 +31,20 @@ public function findLocationByItem($item, $findOnlyOne = false)
$select->where("$alias.item_id = ?", $itemId);
}

// If only a single location is request, return the first one found.
if ($findOnlyOne) {
$location = $this->fetchObject($select);
return $location;
}

// Get the locations.
$locations = $this->fetchObjects($select);

// Return an associative array of locations where the key is the item_id of the location
// Note: Since each item can only have one location, this makes sense to associate a single location with a single item_id.
// However, if in the future, an item can have multiple locations, then we cannot just associate a single location with a single item_id;
// Instead, in the future, we would have to associate an array of locations with a single item_id.
$indexedLocations = [];
foreach ($locations as $k => $loc) {
$indexedLocations[$loc['item_id']] = $loc;
$grouped = [];
foreach ($locations as $loc) {
$grouped[$loc->item_id][] = $loc;
}
return $indexedLocations;
return $grouped;
}

/**
* Add permission check to location queries.
* Join items so that public permissions on items are enforced for locations.
*
* Since all locations belong to an item we can override this method to join
* the items table and add a permission check to the select object.
* Locations have no visibility of their own — a location is public only if
* its item is public. Joining the items table here means every query on
* this table automatically excludes locations for private items.
*
* @return Omeka_Db_Select
*/
Expand Down
2 changes: 1 addition & 1 deletion plugin.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ link="https://omeka.org/classic/docs/Plugins/Geolocation/"
support_link="https://forum.omeka.org/c/omeka-classic/plugins"
omeka_minimum_version="2.5"
omeka_target_version="3.0"
version="3.3"
version="4.0"
2 changes: 0 additions & 2 deletions tests/Geolocation_IntegrationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ public function _addPluginHooksAndFilters($pluginBroker, $pluginName)

// Add plugin filters
add_filter('admin_navigation_main', 'geolocation_admin_nav');
add_filter('define_response_contexts', 'geolocation_kml_response_context');
add_filter('define_action_contexts', 'geolocation_kml_action_context');
add_filter('admin_items_form_tabs', 'geolocation_item_form_tabs');
}
}
4 changes: 2 additions & 2 deletions views/helpers/GeolocationMapBrowse.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public function geolocationMapBrowse($divId = 'map', $options = [], $attrs = [],

if (!array_key_exists('uri', $options)) {
// This should not be a link to the public side b/c then all the URLs that
// are generated inside the KML will also link to the public side.
$options['uri'] = url('geolocation/map.kml');
// are generated inside the JSON will also link to the public side.
$options['uri'] = url('geolocation/map/browse-json');
}

if (!array_key_exists('fitMarkers', $options)) {
Expand Down
5 changes: 5 additions & 0 deletions views/helpers/GeolocationMapOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public function geolocationMapOptions($options = [])

$options['custom_map'] = json_decode((string) get_option('geolocation_custom_map'), true);

$options['strings'] = [
'fitAllMarkers' => __('Fit all markers'),
'label' => __('Label'),
];

return js_escape($options);
}

Expand Down
81 changes: 46 additions & 35 deletions views/helpers/GeolocationMapSingle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,57 @@

class Geolocation_View_Helper_GeolocationMapSingle extends Zend_View_Helper_Abstract
{
public function geolocationMapSingle($item = null, $width = '200px', $height = '200px', $hasBalloonForMarker = false, $markerHtmlClassName = 'geolocation_balloon')
public function geolocationMapSingle($item = null, $width = '200px', $height = '200px')
{
$divId = "item-map-{$item->id}";
$location = get_db()->getTable('Location')->findLocationByItem($item, true);
// Only set the center of the map if this item actually has a location
// associated with it
if ($location) {
$center['latitude'] = $location->latitude;
$center['longitude'] = $location->longitude;
$center['zoomLevel'] = $location->zoom_level;
$center['show'] = true;
if ($hasBalloonForMarker) {
$titleLink = link_to_item(metadata($item, ['Dublin Core', 'Title'], [], $item), [], 'show', $item);
$thumbnailLink = !(item_image('thumbnail')) ? '' : link_to_item(item_image('thumbnail', [], 0, $item), [], 'show', $item);
$description = metadata($item, ['Dublin Core', 'Description'], ['snippet' => 150], $item);
$center['markerHtml'] = '<div class="' . $markerHtmlClassName . '">'
. '<div class="geolocation_balloon_title">' . $titleLink . '</div>'
. '<div class="geolocation_balloon_thumbnail">' . $thumbnailLink . '</div>'
. '<p class="geolocation_balloon_description">' . $description . '</p></div>';
}
$locations = get_db()->getTable('Location')->findBy(['item_id' => $item->id]);

$options = [];
$options['basemap'] = get_option('geolocation_basemap');
$options = $this->view->geolocationMapOptions($options);
$center = js_escape($center);
$varDivId = Inflector::variablize($divId);

$style = "width:$width;height:$height";
$divAttrs = [
'id' => $divId,
'class' => 'map geolocation-map',
'style' => $style,
];
if (empty($locations)) {
return '<p class="map-notification">' . __('This item has no location info associated with it.') . '</p>';
}

$html = '<div ' . tag_attributes($divAttrs) . '></div>';
$js = "var $varDivId" . "OmekaMapSingle = new OmekaMapSingle(" . js_escape($divId) . ", $center, $options); ";
$html .= "<script type='text/javascript'>$js</script>";
} else {
$html = '<p class="map-notification">'.__('This item has no location info associated with it.').'</p>';
// For single-location items this sets the initial zoom correctly.
// For multi-location items fitMarkers() overrides the center after all points are added.
$center = [
'latitude' => $locations[0]->latitude,
'longitude' => $locations[0]->longitude,
'zoomLevel' => $locations[0]->zoom_level,
];

$points = [];
foreach ($locations as $loc) {
$point = [
'latitude' => $loc->latitude,
'longitude' => $loc->longitude,
'zoomLevel' => $loc->zoom_level,
'label' => $loc->label,
];
if ($loc->label !== '') {
$point['markerHtml'] = '<div class="geolocation_balloon">'
. '<div class="geolocation_balloon_title">' . html_escape($loc->label) . '</div>'
. '</div>';
}
$points[] = $point;
}

$options = [];
$options['basemap'] = get_option('geolocation_basemap');
$options['points'] = $points;
$options = $this->view->geolocationMapOptions($options);
$center = js_escape($center);
$varDivId = Inflector::variablize($divId);

$style = "width:$width;height:$height";
$divAttrs = [
'id' => $divId,
'class' => 'map geolocation-map',
'style' => $style,
];

$html = '<div ' . tag_attributes($divAttrs) . '></div>';
$js = "var $varDivId" . "OmekaMapSingle = new OmekaMapSingle(" . js_escape($divId) . ", $center, $options); ";
$html .= "<script type='text/javascript'>$js</script>";

return $html;
}
}
15 changes: 15 additions & 0 deletions views/shared/css/geolocation-marker.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ div#geolocation {
margin-bottom:0px;
}

.leaflet-control-fit-all a {
display: flex !important;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
}

.leaflet-control-fit-all a svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}

img.leaflet-tile,
img.leaflet-marker-icon,
img.leaflet-marker-shadow {
Expand Down
Loading