Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable URL handling for selected layers #53

Open
batpad opened this issue Dec 16, 2024 · 18 comments
Open

Enable URL handling for selected layers #53

batpad opened this issue Dec 16, 2024 · 18 comments

Comments

@batpad
Copy link
Collaborator

batpad commented Dec 16, 2024

Currently, you can share a URL with the position of the map, but you cannot share a URL state that will have the same selected layers, etc. show up for the user opening the URL.

We should be able to encode the entire map state into the URL so we can share URLs that have all the selected layers, and ideally, as well selected locations w/ popups open, etc. to make it easier to share links for specific locations, and specific layer combinations.

This would allow us to craft more precise stories and highlight particular things by sharing the map state more precisely in the URL.

I think this is pretty important to do as the list of data layer grows - it does not help to send someone a link to look at something and then have to tell them to click a hamburger menu and select particular layers - one should be able to select the relevant layers and generate a URL to share.

This is going to be a bit tricky to implement - @planemad @pratapvardhan, I could sketch something out, but this would probably need some thoughtful refactoring of the code to make manageable. Let me know if this seems worth spending time on, and then I can give this some thought and sketch something out.

@planemad
Copy link
Contributor

this is pretty important to do as the list of data layer grows

probably need some thoughtful refactoring of the code to make manageable

This is going to be interesting, let us know if you find snakes.

@pratapvardhan
Copy link
Collaborator

I agree, this will be useful and needed. Happy to help out.

@batpad
Copy link
Collaborator Author

batpad commented Dec 17, 2024

Nice! So, broadly, I think there's two pathways here for implementation:

  1. Full URL state management with history management: With this approach, we would update the URL as a user selects / deselects layers, clicks popups, etc. and at any point a user would be able to copy the current URL and share it. We would also handle back button management, and make sure at all times the current state of the app is synced with the URL. This gets very complex to implement without a framework to handle URL routing, and I think we'd either need to add a lot of brittle complexity to the app OR introduce a javascript framework, and I think we want to avoid either of these things. I think we can take an approach that should be relatively much simpler to implement:

  2. Explicit Permalink button and handle URL params only on page load: With this approach, we would have a button somewhere on the UI like "Share Permalink". When the user clicks that button, we read the current state of the map: selected layers, open popups, etc. and handle encoding the state into URL parameters. I imagine the URL might look something like: https://amche.in?layers=gstm,abc,xyz&selectedItem=13#12.456/15.456/7 - this way we encode things like selected layers and items as query parameters and continue using the hash parameters for the map position. When the page loads, we check the URL, if these query parameters are set, then we have a single function that handles reading the query parameters and setting the active layers, etc. based on them. This way, we don't need to handle constantly keeping the URL in sync with current state, and weird edge cases with trying to handle back button navigation, etc.

I think the second approach should be doable - we'd probably need to think a little bit about how best to represent the URL parameters, and what all we might want to encode in the URL to be able to be shareable in the future. The tasks here would be roughly:

  • Create some UI / design for a "Permalink" button. When user clicks button, it can show the shareable URL of the current state in a text box, and / or also copy to clipboard.
  • Decide and document the URL scheme: what possible query parameters do we need, how do we represent things like layer names, etc. URLs would probably look something like https://amche.in?layers=gstm,abc,xyz&selectedItem=13#12.456/15.456/7, but we should define this clearly.
  • A JS function like getPermalink which would check the current state of the map - active layers, selected popups, etc. and generate the URL string.
  • A JS function like loadMapStateFromURL which will read the query parameters in the URL, and based on that state, set the active layers, show a popup if set, etc. This will need to be called on page load, after the map and layer information has been loaded.

I'll poke around things a bit and see what it takes to get a basic proof of concept going.

@batpad
Copy link
Collaborator Author

batpad commented Dec 17, 2024

let us know if you find snakes.

Don't say such things - next @marlonrodrigues will make a ticket to add a live map of all snakes in Goa 😂

@planemad
Copy link
Contributor

Explicit Permalink button is how it's done for the Singapore Land Map site and works great. This feels like a natural workflow for a user.

licecap2

The urls however are not pretty, but maybe its on purpose?

https://www.ura.gov.sg/maps/?link=N4IgDg9gliBcoBsCGAXOBGAdAZgOwA4AWXATgFZ1sKTCAGdXAGhAQDsBzDW7TE2s-AJIkq2WgCYSAX2YBnAKYIAygDcAxnBAARAKIA1ACoAlHQHEQzALZIwOgB4p5rNPBAB9WRACuKABYB1eVkXRFQMHDpJMgA2WlxCQXjxQmY2Tlh0bkx8fBJccXF8ZNoSaOj8GXdWCAAnPx0kYLhQlyxCTP4BMgTcWMl0VI4uHlLowrwSIoZSKUqAI0b5azA4AG1QKAATTTnLdAtwJFZ5AC0ASVZN+Ts4cXpmCDAkNSgUAE8uWhkN7dgQXfEByex3Ol2utwkDyeL3enykAF0Hip5DVkB9YOsQFtNBAEPggUdThcrjdYIRhFDnq90R0EVIgA

Decide and document the URL scheme: what possible query parameters do we need, how do we represent things like layer names, etc. URLs would probably look something like https://amche.in?layers=gstm,abc,xyz&selectedItem=13#12.456/15.456/7, but we should define this clearly.

👌 Let's start with just having layers to start and add others based on user feedback

@pratapvardhan
Copy link
Collaborator

I generally like having readable url values vs hashed ones. Like what @batpad suggested, https://amche.in?layers=gstm,abc,xyz&selectedItem=13#12.456/15.456/7

@batpad
Copy link
Collaborator Author

batpad commented Dec 19, 2024

I have slightly complex thoughts on this -

While this readable URL structure seems nice at first, I do think we are quickly going to run into more complex state that we will want to store in the URL. For example, I do think we will want to be able to store something like "sub-options" for layers, for things like the Opacity values, Terrain Exxageration, Choice of Data / Map, so that a URL can more exactly reproduce the state of the map the user is looking at / sharing.

At that point, for convenience, I really want to just put JSON in the URL so like layerOpts={'gstm': { opacity: 0.5}, {'terrain': {'exxageration': 22}} and then parse that out to send fairly arbitrary options to different layers. The problem is that JSON in URLs is especially terrible, and I have always regretted a bit doing that when I have. However, constraining oneself to pretty URLs for complex state is also a bit awful and makes some naming decisions really hard, can get things really complicated, and in the end, the URLs can get really long anyways, and as much as I love pretty URLs, I think the trade-offs are often not worth it. I think it would be simpler to run our own little URL shortener service if we wanted to and auto-generate short URLs that map to longer, "ugly" URLs.

I think I actually really like what these ura.sg.gov folks seem to be doing with the URL like https://www.ura.gov.sg/maps/?link=N4IgDg9gliBcoBsCGAXOBGAdAZgOwA4AWXATgFZ1sKTCAGdXAGhAQDsBzDW7TE2s-AJIkq2WgCYSAX2YBnAKYIAygDcAxnBAARAKIA1ACoAlHQHEQzALZIwOgB4p5rNPBAB9WRACuKABYB1eVkXRFQMHDpJMgA2WlxCQXjxQmY2Tlh0bkx8fBJccXF8ZNoSaOj8GXdWCAAnPx0kYLhQlyxCTP4BMgTcWMl0VI4uHlLowrwSIoZSKUqAI0b5azA4AG1QKAATTTnLdAtwJFZ5AC0ASVZN+Ts4cXpmCDAkNSgUAE8uWhkN7dgQXfEByex3Ol2utwkDyeL3enykAF0Hip5DVkB9YOsQFtNBAEPggUdThcrjdYIRhFDnq90R0EVIgA. - it looks like just some sort of base64 encoding or so of the options (I couldn't quite figure out decoding it, but that's what it roughly seems like). I actually like this - it avoids a lot of silly URL encoding issues with having raw JSON in the URL, and looks slightly cleaner - and for complex state, I think the representation of the options is always going to be a bit opaque to the user in the URL.

So I think my proposal would actually be to just represent all the map options as JSON, and then to save the state in the URL, just base64 encode the JSON string.

I think the fundamental consideration is what all we want to store in the URL state. My argument would be that while it would okay to start with just the selected layers and maybe "selected item", which can be represented cleanly in the URL, the feature is incomplete without incorporating things like Opacity for each layer. My head hurts trying to think of how one would represent these kinds of layer options cleanly as query parameters - if it was just Opacity, one could probably come up with something, but there can be a different range of "sub options" for each layer, and we shouldn't have to make changes to the structure of the query parameters every time there's a new type of layer with new options, etc.

It seems really convenient to just have a JSON representation of all these layer options that we can easily deserialize and re-create the map state from and then instead of having a convoluted mechanic to go from the JSON to a "clean" query parameter representation back to the JSON, to just encode the JSON itself in the URL.

I love thinking through stuff like this :) - @pratapvardhan let me know if you have strong feelings either way - happy to chat a bit to try and make the "cleaner URLs" option work, but I think right now I'm leaning toward "base64 encoded JSON as a single query parameter", and if needed down the line consider a URL shortener integration.

@planemad
Copy link
Contributor

I think the fundamental consideration is what all we want to store in the URL state.

The most basic use case to preserve a camera view can be satisfied largely with just the layers setting which is a core feature of this app.

Layer specific settings can be quite large in number and scope like opacity, raster-coloring options, override styling, feature state of the selected map object etc and its not yet clear what the universe of what we would need look like.

We could then keep this ticket focused on adding layers to the core url and separately scope out how to handle external data being passed to the site which could be part of the site API.

@pratapvardhan
Copy link
Collaborator

pratapvardhan commented Dec 19, 2024

@batpad Ah, they are using LZ-based compression on json object. URL you shared decodes to this.

LZString.decompressFromEncodedURIComponent('N4IgDg9gliBcoBsCGAXOBGAdAZgOwA4AWXATgFZ1sKTCAGdXAGhAQDsBzDW7TE2s-AJIkq2WgCYSAX2YBnAKYIAygDcAxnBAARAKIA1ACoAlHQHEQzALZIwOgB4p5rNPBAB9WRACuKABYB1eVkXRFQMHDpJMgA2WlxCQXjxQmY2Tlh0bkx8fBJccXF8ZNoSaOj8GXdWCAAnPx0kYLhQlyxCTP4BMgTcWMl0VI4uHlLowrwSIoZSKUqAI0b5azA4AG1QKAATTTnLdAtwJFZ5AC0ASVZN+Ts4cXpmCDAkNSgUAE8uWhkN7dgQXfEByex3Ol2utwkDyeL3enykAF0Hip5DVkB9YOsQFtNBAEPggUdThcrjdYIRhFDnq90R0EVIgA')

'{
    "poi": {
        "lat": 1.3784795135194017,
        "lng": 103.90585899353029
    },
    "selSvc": "DEVTREG",
    "mapExtent": {
        "_southWest": {
            "lat": 1.3402956074857424,
            "lng": 103.88972282409668
        },
        "_northEast": {
            "lat": 1.4100558548760291,
            "lng": 103.96628379821779
        }
    },
    "basemap": [
        {
            "id": "bm1",
            "paneZIndex": 201,
            "opacity": 100
        },
        {
            "id": "bm2",
            "paneZIndex": 202,
            "opacity": 100
        }
    ],
    "overlay": [
        {
            "id": "ol8",
            "paneZIndex": 499,
            "opacity": 100
        }
    ]
}'

@pratapvardhan
Copy link
Collaborator

pratapvardhan commented Dec 19, 2024

@batpad agree with how you're thinking, don't have very strong opinions. If we really need readable URLs, I'd probably think of flattening the state like this, but may not be useable once we start having too many configuration options to store in state, perhaps? we'll also have to deal with ~2000 character limit and type coercion issues I suspect.

{
    "poi.lat": 1.3784795135194017,
    "poi.lng": 103.90585899353029,
    "selSvc": "DEVTREG",
    "mapExtent._southWest.lat": 1.3402956074857424,
    "mapExtent._southWest.lng": 103.88972282409668,
    "mapExtent._northEast.lat": 1.4100558548760291,
    "mapExtent._northEast.lng": 103.96628379821779,
    "basemap.0.id": "bm1",
    "basemap.0.paneZIndex": 201,
    "basemap.0.opacity": 100,
    "basemap.1.id": "bm2",
    "basemap.1.paneZIndex": 202,
    "basemap.1.opacity": 100,
    "overlay.0.id": "ol8",
    "overlay.0.paneZIndex": 499,
    "overlay.0.opacity": 100
}

@pratapvardhan
Copy link
Collaborator

pratapvardhan commented Dec 19, 2024

As I starting writing above message, I'm leaning towards "base64 encoded JSON as a single query parameter". I think, we should only do this if we decide to store whole set of configuration parameters in URL. But if we will expose only layers list, perhaps read-able URL would be more useful?

@batpad
Copy link
Collaborator Author

batpad commented Dec 19, 2024

I think we can just do both - start simple and add an extra query parameter for extra options as we need. I do want to try and make sure URLs continue to work, so ideally whatever we do now should be backwards compatible in the future.

Shall we do:

  • For now, implement just the active layers handling and have a URL scheme like https://amche.in?layers=gstm,abc,xyz&selectedItem=13#12.456/15.456/7 . I may even take this idea of selectedItem out for now and just implement active layers as a layers= query param.
  • When we want to add more complex options and state handling, we just add another query parameter like options=<base64-json> and for the more complex options go with b64 encoded JSON

How does that sound @planemad @pratapvardhan ?

@marlonrodrigues
Copy link
Collaborator

let us know if you find snakes.

Don't say such things - next @marlonrodrigues will make a ticket to add a live map of all snakes in Goa 😂

If I find it, you'll see it posted here :)))

planemad added a commit that referenced this issue Dec 24, 2024
* slightly messy, first stab at constructing initially checked layers based on query parameters in url

* Remove flash animation of layers on load

---------

Co-authored-by: Arun Ganesh <[email protected]>
@batpad
Copy link
Collaborator Author

batpad commented Dec 24, 2024

With #59, we are handling layers set in the URL state.

@planemad next, we need some UI for users to be able to generate this permalink to be able to share.

What it would roughly need to be:

  • A button somewhere saying Share / Permalink or so.
  • Clicking on it should call a function which checks what layers are currently selected and then constructs a URL string like https://amche.in/?layers=x,y,z#1/2/3 with the selected layer IDs, and keeping the hash URL of the current viewport.
  • That string can be displayed somewhere to the user, but can also just be copied to the user's clipboard with a small message saying "Copied to Clipboard" or so.

@pratapvardhan
Copy link
Collaborator

@batpad I'm thinking, since we aren't adding a bas64 encoded value yet. Should we push the state to URL instead of adding a UI element?

@batpad
Copy link
Collaborator Author

batpad commented Dec 24, 2024

@pratapvardhan I think my main concern with pushing state to the URL is ensuring neat handling of things like back button behaviour. I think if we use history.replaceState instead of history.pushState, we can get around most issues: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState - then the URL will be replaced, but if the user presses the back button, they would be taken to the previous page they were on, and not the previous "state" of layers. I really do not want to get into neatly handling history state changes with back button, etc. - I think this will get too awkward and messy.

I think if we use replaceState, this might be okay - the problem is in the future if we do want to add any history / state changes to the page where we do want the back button to take the user back to the previous state. Not pushing to the URL and just having a button, and then only dealing with URL parameters on page load seemed like a straightforward way to circumvent some of this complexity, but it's entirely possible that I'm just scarred from previous projects where dealing with URL state and especially history management got really tricky.

It was also not fully clear to me if there's a good way to add the URL update code to a single place in the code, or if we'd land up then having to add that to a bunch of different checkbox / click handlers in the code. Just generating the URL on clicking one "Permalink" button again seemed like a way to get around that problem. But I haven 't looked close enough to be sure if this is really a problem.

However, if you feel good about pushing to the URL and feel like there's a reasonably clean path to that, that sounds good to me!

@pratapvardhan
Copy link
Collaborator

@batpad agreed overall. Given #58, (I haven't looked at various parts of the code closely yet 🙈), it might make sense to hold off on adding URL updates for now. Implementing a permalink button first, and then revisiting URL updates after refactoring the code seems like a reasonable approach?

@batpad
Copy link
Collaborator Author

batpad commented Dec 24, 2024

Implementing a permalink button first, and then revisiting URL updates after refactoring the code seems like a reasonable approach?

I think so, yes! Sorry I'm probably going to have limited time until early Jan, so if you want to take a stab at this, please go ahead :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants