Skip to content

Commit

Permalink
Feature/1252 control hajk programatically (#1272)
Browse files Browse the repository at this point in the history
* Initial commit for #1252:
- Added an example page that embeds the locally running Hajk instance.
- Added two buttons to the example. They can be used to control the zoom level.
- Added a listener to App.js that will trigger onhashchange.
- Implemeted changing zoom level by changing the z-value of hash string.

* Added a generic text field that can be used to try out any hash params

* Major addition to index.js that we'll need to investigate:
- In order to allow embedding parent documents to read out some values from our Hajk instance, I had to add an object to the global object of Hajk.
- It's called 'hajkPublicApi', but it's not a real API (yet?). It only exposes the View's zoom limits.
- This can be expanded to a real public API with executable functions, or completely removed if we see problems with this approach.

* Quite a lot in here, but I'm basically done with 1 & 2 of the Methodology, as described in #1252.

* Added 'env: browser' to eslintrc in order to get rid of 'Unknown variable: window'

* Experimental: Added the OL Map object to the public Hajk API.

* Implemented search functionality in the hash API:
- The q paramter is updated when user types in the search bar
- If the map loads with the q parameter OR the q parameter's value changes, search is invoked.
- Added a debouncer to utils, can come in handy in other places too.

* Very much WIP, but I'll commit it for the record:
- AnchorModel now part of core, will get initated no matter the settings for Anchor plugin
- A asynchronous debounce helper takes care of not re-creating the URL too often.
- Anchor plugin needs some more work to work with the new solution. The main question that remains is: how should we treat the 'clean' parameter? If we update it in the URL immitiately, we would expect the map to get cleaned up and the Anchor view would disappear. But if we don't put 'clean' into the hash, it must be taken care of somewhere else, or else it will be overriden the next time user does something that triggers URL regeneration, so that 'clean's value will be set to false again. Fixable but we need to decide on this one.

* Cleaned up the AnchorModel API by making private properties private.

* The 'clean' param is only added to the AnchorView's URL, never to the hash in browser's URL field.

* URLs generated by anchor now only include the hash portion:
- Search params are used only internally but removed prior return
- We still support both search and hash params on init (as we merge them both in AppModel), but I see no need in generating the search params portion now, when we work with the hash portion under the hood.

* A little longer default delay on debounce

* Added support for x & y, plus new param: p:
- Changes to x & y trigger now View animation, just as z change does. This means we have a map that stores its' state in the URL's hash, including full compatibility with browser's history mechanism.
- Added a new param: p. It is meant to store the currently visible plugin(s). With this option, app's state now also includes the most recently shown plugins. They also follow with the link, so when URL is shared, the plugin visibility is also shared.

* Minor fix: separator must by specified when calling .split()

* Some comments and cleanups

* Layer history and toggling almost done:
- As usual, group layers are an issue. They toggel correctly in map, but the sublayers aren't selected correctly in LayerSwitcher. Will need to take a look into that too.
- But I'm commiting now, as this has grown pretty big already. So, two things:
  - l and gl parameters are now respected
  - THIS ENTIRE FUNCTIONALITY IS NOW BEHIND A FLAG IN ADMIN: enableAppStateInHash. I think it's neccessary to make it optional at this stage.

* Fixed functionality for Hajk's group layers:
- I think this is it. What a horrible mess… We must get rid of group layers

* I had a nice long comment explaining the mess. Forgot to put the code next to it, fixes here.

* Removed a log message

* Got rid of a (probably) unneeded timeout.

* Extended the embedded example page.

* Added `enableAppStateInHash` option to dotnet backend

* Fixed scroll-zoom "jump"-issue.

Zoom should be parsed as float - not int

* Fixed another zoom-parsing issue

* Fixed missing delay in debounce

Co-authored-by: Henrik Hallberg <43059093+Hallbergs@users.noreply.github.com>
  • Loading branch information
jacobwod and Hallbergs authored Jan 25, 2023
1 parent 20ae23d commit 476d51c
Show file tree
Hide file tree
Showing 16 changed files with 783 additions and 142 deletions.
2 changes: 2 additions & 0 deletions backend/mapservice/Models/MapSetting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class MapSetting

public bool enableDownloadLink { get; set; }

public bool enableAppStateInHash { get; set; }

public string logo { get; set; }

public string logoLight { get; set; }
Expand Down
29 changes: 28 additions & 1 deletion new-admin/src/views/mapoptions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class MapOptions extends Component {
constrainOnlyCenter: config.constrainOnlyCenter,
constrainResolution: config.constrainResolution,
constrainResolutionMobile: config.constrainResolutionMobile || false,
enableDownloadLink: config.enableDownloadLink,
enableDownloadLink: config.enableDownloadLink || false,
enableAppStateInHash: config.enableAppStateInHash,
altShiftDragRotate: config.altShiftDragRotate || true,
onFocusOnly: config.onFocusOnly || false,
doubleClickZoom: config.doubleClickZoom || true,
Expand Down Expand Up @@ -150,6 +151,7 @@ class MapOptions extends Component {
constrainResolution: mapConfig.constrainResolution,
constrainResolutionMobile: mapConfig.constrainResolutionMobile || false,
enableDownloadLink: mapConfig.enableDownloadLink,
enableAppStateInHash: mapConfig.enableAppStateInHash,
altShiftDragRotate: mapConfig.altShiftDragRotate,
onFocusOnly: mapConfig.onFocusOnly,
doubleClickZoom: mapConfig.doubleClickZoom,
Expand Down Expand Up @@ -347,6 +349,7 @@ class MapOptions extends Component {
case "constrainResolution":
case "constrainResolutionMobile":
case "enableDownloadLink":
case "enableAppStateInHash":
case "altShiftDragRotate":
case "onFocusOnly":
case "doubleClickZoom":
Expand Down Expand Up @@ -414,6 +417,7 @@ class MapOptions extends Component {
"constrainResolutionMobile"
);
config.enableDownloadLink = this.getValue("enableDownloadLink");
config.enableAppStateInHash = this.getValue("enableAppStateInHash");
config.altShiftDragRotate = this.getValue("altShiftDragRotate");
config.onFocusOnly = this.getValue("onFocusOnly");
config.doubleClickZoom = this.getValue("doubleClickZoom");
Expand Down Expand Up @@ -822,6 +826,29 @@ class MapOptions extends Component {
/>
</label>
</div>
<div>
<input
id="input_enableAppStateInHash"
type="checkbox"
ref="input_enableAppStateInHash"
onChange={(e) => {
this.setState({ enableAppStateInHash: e.target.checked });
}}
checked={this.state.enableAppStateInHash}
/>
&nbsp;
<label
className="long-label"
htmlFor="input_enableAppStateInHash"
>
Beta: aktivera liveuppdatering av hashparametar i URL-fältet{" "}
<i
className="fa fa-question-circle"
data-toggle="tooltip"
title="Kartans status hålls ständigt uppdaterad, som en del av URL:ens #-parametrar. Se även #1252."
/>
</label>
</div>
<div className="separator">Kartinteraktioner</div>
<div>
Se{" "}
Expand Down
3 changes: 3 additions & 0 deletions new-client/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"extends": ["react-app", "react-app/jest", "prettier"],
"plugins": ["prettier"],
"env": {
"browser": true
},
"rules": {
"prettier/prettier": "error",
"arrow-body-style": "off",
Expand Down
184 changes: 184 additions & 0 deletions new-client/public/examples/embedded.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Example: Hajk Embedded</title>
<style>
p {
max-width: 660px;
}
</style>
</head>

<body style="max-width: 1280px">
<h1>How to embed and control Hajk</h1>

<form>
<fieldset>
<legend>Full control:</legend>
<input id="hashField" type="text" size="140" />
<button id="updateButton">Update IFRAME</button>
</fieldset>
<fieldset>
<legend>Quick buttons:</legend>
<button id="increaseButton">Increase zoom</button>
<button id="decreaseButton">Decrease zoom</button>
</fieldset>
</form>
<iframe id="iframe" src="/" width="1280" height="768"></iframe>

<p>
This example shows how an embedded Hajk instance can be controlled from
the embedding website.
</p>
<p>
<b
>It highlights the two-way interaction between the <i>embedded</i> Hajk
application and the <i>embedding</i> (parent) page.
</b>
</p>
<p>
You can use the <i>Quick buttons</i> to modify the <code>z</code> (zoom)
parameter, or use the <i>Full control</i> input field to freely set any
parameter.
</p>
<p>Currently the following parameters are supported by the API:
<ul>
<li><code>x</code> and <code>y</code> - map's center coordinates</li>
<li><code>z</code> - map's zoom level</li>
<li><code>p</code> - visible plugins</li>
<li><code>l</code> - visible layers</li>
<li><code>gl</code> - visible sublayers of group layers</li>
<li><code>q</code> - search query string</li>
</ul>
</p>
<p>
Hajk listens to changes to the hash parameters and acts accordingly, e.g.
by changing the map's zoom level. The input field's value (above) is being kept
up-to-date with Hajk's current value, so that <b>you can easily see how the
hash parameters change as you do things in the application</b>.
</p>

<p>
<i>
This example is accessible from
http://localhost:3000/examples/embedded.html when running Hajk in
development mode.</i
>
</p>

<p>
<i>
For more info on background and development, see issue <a href="https://github.com/hajkmap/Hajk/issues/1252" target="_blank">#1252</a>.</i
>
</p>
<script type="module">
// We start off with an empty hash
// const currentHash = new URLSearchParams("/");

// Grab some elements we want to access
const iframe = document.getElementById("iframe");
const hashField = document.getElementById("hashField");
const updateButton = document.getElementById("updateButton");
const increaseButton = document.getElementById("increaseButton");
const decreaseButton = document.getElementById("decreaseButton");

let hajkPublicApi = {
minZoom: 2,
maxZoom: 20,
};

const changeZoom = (increaseBy = 1) => {
const url = new URL(iframe.src);

// Extract hash params
const hash = new URLSearchParams(url.hash.replaceAll("#", ""));

// Try to grab current zoom level from hash params, default to "2"
const currentZ = parseInt(hash.get("z")) || 2;

let newZ = currentZ + increaseBy;
if (newZ < hajkPublicApi.minZoom) {
alert(`Can't zoom below level ${hajkPublicApi.minZoom}`);
newZ = hajkPublicApi.minZoom;
}

if (newZ > hajkPublicApi.maxZoom) {
alert(`Can't zoom above level ${hajkPublicApi.maxZoom}`);
newZ = hajkPublicApi.maxZoom;
}

// Set the property of the URLSearchParams object. This will either
// add "z", or update if already exists
hash.set("z", newZ);

// Transform the URLSearchParams object to a valid hash string…
const newHash = "#" + hash.toString();

// …and add to our URL.
url.hash = newHash;

// Finally, let's update the IFRAME's SRC attribute
iframe.src = url.toString();

// For the purpose of this test, let's update hash text field
hashField.value = hash.toString();
};

// Bind the listeners

iframe.contentWindow.addEventListener("hashchange", (e) => {
hashField.value = iframe.contentDocument.location.hash.replaceAll(
"#",
""
);
});

iframe.addEventListener("load", (e) => {
// Wait a second to ensure that JS is processed after Hajk is loaded in
// the IFRAME.
setTimeout(() => {
// Retrieve some data from public API
hajkPublicApi = {
...hajkPublicApi,
...iframe.contentWindow.hajkPublicApi,
};

// Ensure that the hash text field here corresponds to the actual from
// the embedded document (Hajk will change its hash string on init)
hashField.value = iframe.contentDocument.location.hash.replaceAll(
"#",
""
);
}, 700);
});

updateButton.addEventListener("click", (e) => {
e.preventDefault();
// Create the new hash value by taking the current hash field value
// and parse it into an params object
const newHash = new URLSearchParams(hashField.value).toString();

// Grab the URL (will be used to replace the SRC param of the IFRAME soon)
const url = new URL(iframe.src);
url.hash = "#" + newHash;

// Update IFRAME's SRC attribute
iframe.src = url.toString();

// Finally, let's ensure our hash field has correctly formatted
// params string too, with encoded values, e.g. ","->"%2C"
hashField.value = newHash;
});

increaseButton.addEventListener("click", (e) => {
e.preventDefault();
changeZoom(1);
});

decreaseButton.addEventListener("click", (e) => {
e.preventDefault();
changeZoom(-1);
});
</script>
</body>
</html>
Loading

0 comments on commit 476d51c

Please sign in to comment.