Skip to content

Commit d486562

Browse files
committed
fix interaction with markers and line geometries
1 parent 2c34ab7 commit d486562

11 files changed

Lines changed: 239 additions & 65 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Internationalisation, configurability, and HTML-head metadata — target release
5252
- Geometries crossing the antimeridian (180° meridian) are now stored as RFC 7946 §3.1.9-compliant `MultiPolygon` / `MultiLineString` features split into one part per hemisphere. A new `AntimeridianSplitter` runs on the `Publication::edit` hook so every stored record is unambiguous; legacy pre-fix records render correctly in-memory and are normalised on the next save. The admin-unit overlay for countries with `east<west` bboxes (Russia, New Zealand, Fiji, parts of the US) draws two rectangles across the dateline instead of flipping to the wrong hemisphere. `Centroid` now handles crossing envelopes and skips MySQL's Cartesian `ST_Envelope` for MultiPolygons that straddle ±180°, so `ICBM` / `geo.position` tags are correct. All four maps enable Leaflet's `worldCopyJump`, and a small informational note below the editing maps explains the split-on-save behaviour ([#60](https://github.com/TIBHannover/geoMetadata/issues/60)).
5353
- Translated the admin-unit input-validation alerts on the submission form and the publication tab; previously these were always shown in English regardless of the journal locale ([#110](https://github.com/TIBHannover/geoMetadata/issues/110)).
5454
- The "gazetteer unavailable" notice on the submission form and publication tab now explains the cause — missing GeoNames Base URL, missing username, invalid credentials, daily quota exceeded, or generic external error — instead of a single generic line. A defensive guard on the autocomplete request also surfaces a quota-exhausted state mid-session, and the plugin settings form rejects a save with bad GeoNames credentials inline using the same translated reasons. All five reasons are translated into `en_US`, `de_DE`, `fr_FR`, and `es_ES` ([#164](https://github.com/TIBHannover/geoMetadata/issues/164)).
55+
- Older publication versions (`/article/view/{id}/version/{N}`) now emit the article's geo metadata in the HTML head — `DC.Coverage` for the administrative unit, plus the plugin's other geo meta tags — so each version is independently discoverable. OJS's bundled DublinCoreMetaPlugin previously suppressed `DC.Coverage` on `/version/` URLs ([#102](https://github.com/TIBHannover/geoMetadata/issues/102)).
56+
- Pressing Escape now closes the issue/journal-map article popup regardless of which element holds keyboard focus, and the icon-hover map highlight now lights up the article's geometry whether or not the synced-highlight admin toggle is on. The reset-view control no longer drifts during the post-init `fitBounds` chain — its captured view is the post-async-fitBounds view, and the reset itself is now an instant snap rather than a smooth animation, so a user that pans away always returns to the same place.
57+
- Hover and click hit-testing on the issue and journal maps now matches the visible feature footprint at every zoom level. Hovering anywhere on a marker icon — not just its bottom tip — highlights the article and registers a click; the click target on a polyline now follows the pointer cursor over the visible stroke. Previously the marker hit zone was a small circle around the icon's lat/lng anchor and the polyline hit zone used a 100m geometric tolerance, both of which routinely missed clicks the cursor had clearly landed on ([#81](https://github.com/TIBHannover/geoMetadata/issues/81), [#159](https://github.com/TIBHannover/geoMetadata/issues/159)).
58+
- Journal-wide map and timeline now show one entry per article when an article has multiple published versions; previously every version was rendered, stacking duplicate geometries on the map and breaking the timeline strip with a duplicate-id error from vis-timeline.
5559

5660
## [1.0.1.0] - 2025-10-14
5761

GeoMetadataPlugin.inc.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,11 @@ function extendArticleView($hookName, $args)
360360
{
361361
$request = $args[0];
362362
$article = $args[2];
363-
$publication = $article->getCurrentPublication();
363+
// args[3] is the publication OJS resolved for this view — for
364+
// /article/view/{id}/version/{N} it's the requested older version,
365+
// for the canonical URL it's getCurrentPublication(). Fall back to
366+
// current if the hook signature changes upstream.
367+
$publication = $args[3] ?? $article->getCurrentPublication();
364368

365369
$emitDC = $this->isFeatureEnabled('geoMetadata_emitMetaDublinCore');
366370
$emitGeoNames = $this->isFeatureEnabled('geoMetadata_emitMetaGeoNames');
@@ -399,6 +403,27 @@ function extendArticleView($hookName, $args)
399403
$dcTagAdded = true;
400404
}
401405

406+
// DC.Coverage: OJS's DublinCoreMetaPlugin emits this from the article's
407+
// (submission-level, latest-version) coverage field on canonical URLs
408+
// but explicitly suppresses it on /article/view/{id}/version/{N} URLs
409+
// (see DublinCoreMetaPlugin::handleArticleHook). For the plugin's own
410+
// per-version data to be discoverable on older versions, emit a
411+
// publication-specific DC.Coverage from the requested publication's
412+
// admin-unit hierarchy ONLY on the /version/ URLs — on the canonical
413+
// URL OJS already emits a (locale-tagged) DC.Coverage and we don't
414+
// want duplicates.
415+
$requestArgs = $request->getRequestedArgs();
416+
$onVersionUrl = isset($requestArgs[1]) && $requestArgs[1] === 'version';
417+
if ($emitDC && $hasAdminUnits && $onVersionUrl) {
418+
$names = array_map(function ($u) { return $u->name; }, $decodedAdminUnits);
419+
$names = array_values(array_filter($names, function ($n) { return $n !== null && $n !== ''; }));
420+
if (!empty($names)) {
421+
$templateMgr->addHeader('dublinCoreCoverageGeoMetadata',
422+
'<meta name="DC.Coverage" content="' . htmlspecialchars(implode(', ', $names)) . '" />');
423+
$dcTagAdded = true;
424+
}
425+
}
426+
402427
if ($emitGeoCoords) {
403428
$centroid = null;
404429
$provenance = null;

classes/handler/JournalMapHandler.inc.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,32 @@ public function index($args, $request)
4545
$userGroupDao = DAORegistry::getDAO('UserGroupDAO');
4646
$userGroups = $userGroupDao->getByContextId($context->getId())->toArray();
4747

48+
// Keep one entry per submission (the latest version). When an article
49+
// has multiple published versions, the publications service returns
50+
// each one — without this dedupe the journal map paints overlapping
51+
// copies of the same article and the timeline would reject duplicate
52+
// ids.
53+
$latestBySubmission = [];
4854
foreach ($publications as $publication) {
49-
$id = $publication->getData('id');
50-
51-
if($publication->getData('status') != STATUS_PUBLISHED) {
52-
continue;
55+
if ($publication->getData('status') != STATUS_PUBLISHED) continue;
56+
$sid = $publication->getData('submissionId');
57+
$version = (int) $publication->getData('version');
58+
if (!isset($latestBySubmission[$sid])
59+
|| $version > (int) $latestBySubmission[$sid]->getData('version')) {
60+
$latestBySubmission[$sid] = $publication;
5361
}
62+
}
63+
64+
foreach ($latestBySubmission as $publication) {
65+
$id = $publication->getData('id');
5466

5567
$issue = "";
5668
if ($publication->getData('issueId')) {
5769
$issueDao = DAORegistry::getDAO('IssueDAO');
5870
$issue = $issueDao->getById($publication->getData('issueId'));
5971
$issue = $issue->getIssueIdentification();
6072
}
61-
73+
6274
$publicationsGeodata[$id] = [
6375
'publicationId' => $publication->getData('id'),
6476
'submissionId' => $publication->getData('submissionId'),

cypress/e2e/integration/61-issue-map-icon.cy.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ describe('geoMetadata Issue Map Icon', function () {
7575
cy.contains('.obj_article_summary', 'Hanover is nice')
7676
.find('a.geoMetadata_issue_mapIcon')
7777
.trigger('mouseenter');
78-
// geometry's stroke switches to the configured highlight colour (default #FF0000).
79-
cy.get('#mapdiv path.leaflet-interactive').should('have.attr', 'stroke', '#ff0000');
78+
// At least one path now carries the configured highlight colour. Asserting
79+
// "any" rather than "the first" makes the test robust to insertion order
80+
// among seeded articles (more were added for the timeline / overlap tests).
81+
cy.get('#mapdiv path.leaflet-interactive[stroke="#ff0000"]', { timeout: 10000 }).should('exist');
8082
});
8183

8284
it('click on the icon opens the article popup and scrolls the map into view', function () {

cypress/e2e/integration/62-reset-view.cy.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ describe('geoMetadata Reset View Button', function () {
2121

2222
const captureCentre = () => cy.window().then((win) => win.map.getCenter());
2323

24+
// Reads the snapshot the control will reset to. Using this rather than
25+
// capturing the centre at an arbitrary moment removes timing assumptions
26+
// about when the post-init fitBounds chain settles — the control and the
27+
// assertion now agree on the same source of truth by construction.
28+
const captureControlSnapshot = () => cy.window().then((win) => {
29+
const ctrl = win.map._geoMetadataResetView;
30+
expect(ctrl, 'reset-view control instance exposed on map').to.exist;
31+
return { lat: ctrl._initialCenter.lat, lng: ctrl._initialCenter.lng };
32+
});
33+
2434
const assertResetLandsBackAtOrigin = () => {
2535
cy.get(resetBtn, { timeout: 20000 }).should('exist').and('have.attr', 'title', 'Reset view');
36+
// Wait past the control's hard-cap freeze window so the snapshot is stable.
37+
cy.wait(3000);
2638
let origin;
27-
captureCentre().then((c) => { origin = { lat: c.lat, lng: c.lng }; });
28-
cy.window().then((win) => win.map.panBy(panOffset));
39+
captureControlSnapshot().then((c) => { origin = c; });
40+
// animate:false keeps the centre update synchronous so the next getCenter()
41+
// is guaranteed to reflect the pan.
42+
cy.window().then((win) => win.map.panBy(panOffset, { animate: false }));
2943
captureCentre().then((after) => {
3044
// Sanity: the pan actually moved the centre (otherwise the test is a no-op).
3145
expect(Math.abs(after.lat - origin.lat) + Math.abs(after.lng - origin.lng)).to.be.greaterThan(tolerance);
@@ -61,8 +75,14 @@ describe('geoMetadata Reset View Button', function () {
6175
});
6276

6377
it('on the submission map', function () {
64-
cy.login('tobler', 'tobler', Cypress.env('contexts').primary.path);
65-
cy.visit('/' + Cypress.env('contexts').primary.path + '/submission/wizard');
78+
cy.openSubmissionsAs('aauthor');
79+
cy.get('div#myQueue a:contains("New Submission")').click();
80+
cy.get('input[id^="checklist-"]').click({ multiple: true });
81+
cy.get('input[id="privacyConsent"]').click();
82+
cy.get('button.submitFormButton').click();
83+
cy.wait(1000);
84+
cy.get('button.submitFormButton').click();
85+
cy.wait(2000);
6686
cy.get('#mapdiv', { timeout: 20000 }).should('be.visible');
6787
assertResetLandsBackAtOrigin();
6888
});

cypress/e2e/integration/66-overlap-popup.cy.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ const visitVol1No2 = () => {
2424
cy.get('a:contains("Vol. 1 No. 2 (2022)")').click();
2525
cy.get('#mapdiv').should('exist');
2626
cy.window().its('map').should('exist');
27+
// articleLayersMap is populated asynchronously after issue.js finishes
28+
// attaching all article layers (and the timeline-feature additions made
29+
// it more deferred). Wait for layers to settle before tests poke the
30+
// overlap helper.
31+
cy.wait(3000);
32+
// Pin a zoom where the test latlngs are pixel-distinct. Hit-testing is in
33+
// pixel space (matches the visible stroke / icon footprint).
34+
cy.window().then((win) => win.map.setView(win.L.latLng(52.4, 8.7), 8, { animate: false }));
2735
};
2836

2937
describe('geoMetadata Overlap Picker - pure helpers', function () {
@@ -53,6 +61,9 @@ describe('geoMetadata Overlap Picker - pure helpers', function () {
5361
it('pointOnMarker uses pixel tolerance', function () {
5462
cy.window().then((win) => {
5563
const map = win.map;
64+
// Pin zoom — pixel tolerance is zoom-dependent. At very low zoom 1 deg
65+
// collapses to a few pixels, making the "far" assertion below ambiguous.
66+
map.setView(win.L.latLng(52.37, 8.43), 4, { animate: false });
5667
const marker = win.L.latLng(52.37, 8.43);
5768
expect(win.geoMetadata_pointOnMarker(map, marker, [8.43, 52.37]), 'centre').to.be.true;
5869
// 1 deg lng at 52° lat is roughly 68 km, way more than 10 px at zoom 4
@@ -176,7 +187,9 @@ describe('geoMetadata Overlap Picker - toggle off', function () {
176187
setToggle('geoMetadata_overlapPicker', false);
177188
visitVol1No2();
178189
cy.window().then((win) => {
179-
expect(win.geoMetadata_overlapPicker, 'picker disabled').to.be.false;
190+
// geoMetadata_overlapPicker is a script-scoped const in _map_js_globals.tpl,
191+
// so it is not a property of window — read it via win.eval.
192+
expect(win.eval('geoMetadata_overlapPicker'), 'picker disabled').to.be.false;
180193
expect(win.geoMetadata_overlapManager, 'no manager instantiated').to.be.null;
181194
});
182195
cy.window().then((win) => {
@@ -192,7 +205,7 @@ describe('geoMetadata Overlap Picker - toggle off', function () {
192205
setToggle('geoMetadata_overlapPicker', true);
193206
visitVol1No2();
194207
cy.window().then((win) => {
195-
expect(win.geoMetadata_overlapPicker, 'picker re-enabled').to.be.true;
208+
expect(win.eval('geoMetadata_overlapPicker'), 'picker re-enabled').to.be.true;
196209
});
197210
});
198211
});

cypress/e2e/integration/67-multi-feature-highlight.cy.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ describe('geoMetadata Multi-Feature Synced Highlight', function () {
9696
cy.visit('/' + Cypress.env('contexts').primary.path + '/map');
9797
cy.get('#mapdiv').should('exist');
9898
cy.window().its('articleLayersMap').should('exist');
99+
cy.wait(3000);
99100

100101
cy.window().then((win) => {
101102
expect(win.eval('geoMetadata_enableSyncedHighlight'), 'synced-highlight toggle on by default').to.be.true;
@@ -121,7 +122,7 @@ describe('geoMetadata Multi-Feature Synced Highlight', function () {
121122
if (layer.feature.geometry.type === 'Point') {
122123
expect(layer.options.icon.options.className).to.equal('geoMetadata_marker_default');
123124
} else {
124-
expect(layer.options.color).to.equal(win.geoMetadata_mapLayerStyle.color);
125+
expect(layer.options.color).to.equal(win.eval('geoMetadata_mapLayerStyle').color);
125126
}
126127
});
127128

@@ -139,7 +140,7 @@ describe('geoMetadata Multi-Feature Synced Highlight', function () {
139140
'point DOM <img> carries the highlight class').to.contain('geoMetadata_marker_highlight');
140141
} else {
141142
expect(layer.options.color,
142-
'polygon stroke flipped to highlight').to.equal(win.geoMetadata_mapLayerStyleHighlight.color);
143+
'polygon stroke flipped to highlight').to.equal(win.eval('geoMetadata_mapLayerStyleHighlight').color);
143144
}
144145
});
145146

@@ -155,7 +156,7 @@ describe('geoMetadata Multi-Feature Synced Highlight', function () {
155156
if (layer.feature.geometry.type === 'Point') {
156157
expect(layer.options.icon.options.className).to.equal('geoMetadata_marker_default');
157158
} else {
158-
expect(layer.options.color).to.equal(win.geoMetadata_mapLayerStyle.color);
159+
expect(layer.options.color).to.equal(win.eval('geoMetadata_mapLayerStyle').color);
159160
}
160161
});
161162
});
@@ -176,6 +177,8 @@ describe('geoMetadata Multi-Feature Synced Highlight', function () {
176177

177178
cy.visit('/' + Cypress.env('contexts').primary.path + '/map');
178179
cy.get('#mapdiv').should('exist');
180+
// articleLayersMap is populated asynchronously by journal.js DOM-ready code.
181+
cy.wait(3000);
179182
cy.window().then((win) => {
180183
expect(win.eval('geoMetadata_enableSyncedHighlight'), 'toggle now off').to.be.false;
181184
const targetId = findTargetArticleId(win);

cypress/e2e/integration/68-overlap-hover.cy.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const visitVol1No2 = () => {
3333
cy.get('a:contains("Vol. 1 No. 2 (2022)")').click();
3434
cy.get('#mapdiv').should('exist');
3535
cy.window().its('map').should('exist');
36+
// articleLayersMap is populated asynchronously after issue.js attaches the
37+
// article layers; firing mousemove before it settles produces zero hits.
38+
cy.wait(3000);
39+
// Pin a zoom where the test latlngs are pixel-distinct. Hit-testing is in
40+
// pixel space (matches the visible stroke / icon footprint), so at world-fit
41+
// zoom an off-line latlng can fall inside the visible stroke of a far-away
42+
// line and produce a false positive.
43+
cy.window().then((win) => win.map.setView(win.L.latLng(52.4, 8.7), 8, { animate: false }));
3644
};
3745

3846
describe('geoMetadata Overlap Hover Highlight - issue map', function () {

js/issue.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function geoMetadata_openArticlePopup(articleId) {
125125
var layers = articleLayersMap.get(articleId) || [];
126126
if (!layers.length) return;
127127
var mapEl = document.getElementById('mapdiv');
128-
if (mapEl && mapEl.scrollIntoView) mapEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
128+
if (mapEl && mapEl.scrollIntoView) mapEl.scrollIntoView({ block: 'center' });
129129
if (geoMetadata_overlapManager) {
130130
geoMetadata_overlapManager.openArticle(articleId);
131131
} else {
@@ -228,17 +228,54 @@ $(function () {
228228
let input = document.querySelector('.geoMetadata_data.articleId[value="' + articleId + '"]');
229229
let link = input ? geoMetadata_injectIcon(input) : null;
230230
if (link) {
231+
// First on-geometry latlng for the article — used to fan
232+
// out the icon-hover highlight to overlapping articles via
233+
// the map mousemove path (the bounds centre would miss for
234+
// irregular polylines / non-convex polygons).
235+
var firstLatLngOf = function (latlngs) {
236+
while (Array.isArray(latlngs) && latlngs.length) {
237+
if (latlngs[0] && typeof latlngs[0].lat === 'number') return latlngs[0];
238+
latlngs = latlngs[0];
239+
}
240+
return null;
241+
};
242+
var centroidFor = function () {
243+
var layers = articleLayersMap.get(articleId) || [];
244+
for (var i = 0; i < layers.length; i++) {
245+
var layer = layers[i];
246+
if (layer.getLatLng) return layer.getLatLng();
247+
if (layer.getLatLngs) {
248+
var ll = firstLatLngOf(layer.getLatLngs());
249+
if (ll) return ll;
250+
}
251+
}
252+
return null;
253+
};
231254
link.addEventListener('mouseenter', () => {
232-
if (geoMetadata_enableSyncedHighlight) highlightArticleFeatures(articleId);
255+
highlightArticleFeatures(articleId);
256+
highlightArticle(articleId);
257+
if (geoMetadata_enableSyncedHighlight) {
258+
var ll = centroidFor();
259+
if (ll) map.fire('mousemove', { latlng: ll });
260+
}
233261
});
234262
link.addEventListener('mouseleave', () => {
235-
if (geoMetadata_enableSyncedHighlight) resetHighlightArticleFeatures(articleId);
263+
resetHighlightArticleFeatures(articleId);
264+
resetHighlightArticle(articleId);
265+
if (geoMetadata_enableSyncedHighlight) map.fire('mouseout');
236266
});
237267
link.addEventListener('focus', () => {
238-
if (geoMetadata_enableSyncedHighlight) highlightArticleFeatures(articleId);
268+
highlightArticleFeatures(articleId);
269+
highlightArticle(articleId);
270+
if (geoMetadata_enableSyncedHighlight) {
271+
var ll = centroidFor();
272+
if (ll) map.fire('mousemove', { latlng: ll });
273+
}
239274
});
240275
link.addEventListener('blur', () => {
241-
if (geoMetadata_enableSyncedHighlight) resetHighlightArticleFeatures(articleId);
276+
resetHighlightArticleFeatures(articleId);
277+
resetHighlightArticle(articleId);
278+
if (geoMetadata_enableSyncedHighlight) map.fire('mouseout');
242279
});
243280
link.addEventListener('click', (e) => {
244281
e.preventDefault();

0 commit comments

Comments
 (0)