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

Add projection expression syntax #888

Conversation

birkskyum
Copy link
Member

@birkskyum birkskyum commented Nov 4, 2024

merging branch here

Upcoming syntax:

See Video examples here

projection: { type: VALUE }

VALUE can be either of below

  • string (projection) - "mercator" / "vertical-perspective"
  • [string, string, number] - [vertical-perspective, mercator, 0.2]
  • step Expression
["step", ["zoom"],
"vertical-perspective", 
11, "mercator"] -> string
  • interpolate-projection Expression
["interpolate-projection", ["linear"], ["zoom"], 
10, "vertical-perspective", 
12, "mercator"] -> string | [string, string, number]
  • string (preset expression) - "globe" (we'll replace it in the client with either the shader-globe, or with an interpolate-projection expression like above.

Just one of many motivations for this feature is the need for a non-adaptive globe for the reasons here

Generally, we need a way to specify adaptive and non-adaptive transitions in the style spec, with stops, and we need much more flexibility regarding projections, where this syntax is future proof for +proj and WKT strings support.

@codecov-commenter
Copy link

codecov-commenter commented Nov 4, 2024

Codecov Report

Attention: Patch coverage is 89.13043% with 10 lines in your changes missing coverage. Please review.

Please upload report for BASE (projection-expression-collecting-branch@4ebf797). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/expression/definitions/interpolate.ts 77.77% 4 Missing ⚠️
src/util/projection.ts 82.60% 4 Missing ⚠️
src/expression/values.ts 85.71% 1 Missing ⚠️
src/util/interpolate.ts 80.00% 1 Missing ⚠️
Additional details and impacted files
@@                            Coverage Diff                             @@
##             projection-expression-collecting-branch     #888   +/-   ##
==========================================================================
  Coverage                                           ?   92.63%           
==========================================================================
  Files                                              ?      107           
  Lines                                              ?     4749           
  Branches                                           ?     1346           
==========================================================================
  Hits                                               ?     4399           
  Misses                                             ?      350           
  Partials                                           ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@HarelM
Copy link
Collaborator

HarelM commented Nov 4, 2024

The problem with the style spec is that we need to support it "forever", so I don't want to introduce something that later on will cause maintenance issues.
Right now, with this suggestion, globe-to-mercator has an implicit definition of when to switch from globe to mercator, one the user can't choose. We've seen this issue in the default font, that we need to introduce a new definition to overcome the default implicit font.
This is not great.
We can decide to support only a subset of expressions now to make the style spec future proof on one hand but limited to what we can currently support in GL JS on the other.
I've been dealing with pains related to the style spec for a while now, and for me this looks like a future issue...

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

@HarelM , I understand what you mean.

globe-to-mercator has an implicit definition

It's the implicit definitions that we both dislike. The globe-to-mercator in the spec have implicit thresholds, which is why you focus on expressions. I focus on the implicit-adaptive globe, which is why i try to change it to be either explicit adaptive globe-to-mercator or explicit non-adaptive globe. The non-adaptive globe is very explicit, since it has no adaption thresholds, and map well to expressions in the future, or extending to globe-to-mercator should we take that route anyway.

Taking a more conservative, forward looking, approach, I've changed this PR to remove the globe-to-mercator, meaning that there's just the globe and the mercator, both non-adaptive. maplibre/maplibre-gl-js#4977 would go a long way towards making globe work standalone.

I've opened a PR here to align with that strategy

@birkskyum birkskyum changed the title Add non-adaptive globe projection Add globe and mercator expression syntax Nov 5, 2024
@birkskyum birkskyum changed the title Add globe and mercator expression syntax Add projection expression syntax Nov 5, 2024
@birkskyum birkskyum force-pushed the add-non-adaptive-globe branch 2 times, most recently from 06496eb to d1cad9d Compare November 5, 2024 05:39
@birkskyum birkskyum marked this pull request as draft November 5, 2024 06:05
@birkskyum birkskyum marked this pull request as ready for review November 5, 2024 07:06
@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

I've designed an expression based on the step. It doesn't interpolate, so that has to happen in the implementation, which it does already. Is it something like this you have in mind?

Each transition between projections would be a hard cut untill a specific adapter is introduced, like the globe-to-mercator adapter we have.

jest.config.ts Outdated Show resolved Hide resolved
build/generate-style-spec.ts Outdated Show resolved Hide resolved
@HarelM
Copy link
Collaborator

HarelM commented Nov 5, 2024

I think this is in the right direction, thanks!
How would the interpolation go then?
I think for example, color interpolation code is written in this repo, should there be an interpolation code for projection type here as well?
If this is not needed here, that's great too.

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

So since it's a step, there's no interpolation in the expression. If we can get an interpolation value between each step from 0-1 that would be cool, i just don't know how to do it because it says "interpolation not allowed for strings", but I imagine it just need it's own type defined.

We need adapters for the combination, so say it's:
10, 'globe',
12. 'mercator,

Then we have to look up if we have an adapter, like globe-to-mercator.

If we don't have an adapter, it would make a hard cut from one to the other.

If we do have, then we can interpolate and apply the transition, which here is named globeness. We might be able to generalize the transitions, by making a transition from each layer to mercator, and then they can go through that to each other, like an Internal Representation.

If we have
mercator < - > equalEarth
mercator < - > globe

Then a style like this should be possible by going through mercator.

[linear],
`equalEarth`
12, 'globe',

There would ofc be a speedup by having a direct globe < - > equalEarth, but I think it's for good reason that Google Translate and LLVM has translation layers, and mercator seem like an obvious choice albeit more because of tradition than technical reasons .

@HarelM
Copy link
Collaborator

HarelM commented Nov 5, 2024

Besides the comment above this looks good.
I've added a comment in the other PR about using npm link. Let me know in which direction you would like to proceed!
Thanks for all the work around this! Truly appreciated!

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

@HarelM , I have some troubles running the mkdocs. It seems like recursion error in some social plugin.

    return self._resolve_font(family, fallback)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/material/plugins/social/plugin.py", line 504, in _resolve_font
    return self._resolve_font(family, fallback)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/material/plugins/social/plugin.py", line 504, in _resolve_font
    return self._resolve_font(family, fallback)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 977 more times]
  File "/usr/local/lib/python3.11/site-packages/material/plugins/social/plugin.py", line 472, in _resolve_font
    path = os.path.join(self.config.cache_dir, "fonts", family)
                        ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/mkdocs/config/base.py", line 97, in __get__
    if not isinstance(obj, Config):
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen abc>", line 119, in __instancecheck__
RecursionError: maximum recursion depth exceeded in comparison

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

Ah, I had an old .cache folder in root - just deleted it

@HarelM
Copy link
Collaborator

HarelM commented Nov 5, 2024

I think I had it too, don't remember how I solved it, you can comment out the social part in the mkdocs config to overcome this locally, just make sure you don't commit this change.

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

@HarelM , I've implemented a syntax in the style-spec that I'm satisfied with for the time being:

string - No transition:

"projection": {
        "type": "mercator"
}

step - Non-adaptive transition

"projection": {
        "type": [
          "step",
          ["zoom"],
          "globe",
          11, "mercator"
        ]
      }

Will output at a given zoom level i.e. "mercator"

interpolate-projection - Adaptive transitions

"projection": {
        "type": [
          "interpolate-projection",
          ["linear"],
          ["zoom"],
          0, "globe",
          10, "globe"
          12, "mercator"
        ]
      }

Will at any point output i.e. for zoom 11.5: ["globe", "mercator", 0.75] ( altertively ["globe-to-mercator", 0.75] )

We have room in the implementation to take shortcuts, like if we see zoom 5 as ["globe", "globe", 0.5], then we could take a non-adaptive path.


My challenge now is that I don't know how to retrieve the cameraexpression output in gl js

@HarelM
Copy link
Collaborator

HarelM commented Nov 5, 2024

You should be able to call evaluate and give it the relevant zoom, feature state and tile id, I think.
given that we have interpolate-hcl and lab, interoperate-proj (or maybe use the full word, IDK) sounds like a good idea.

I'm not sure about the return value though.
I would expect the return value to be a Projection type maybe, like a new kind of object that we add to the style spec, like color or padding, or resolved Image.
https://maplibre.org/maplibre-style-spec/types/

In theory (not relevant to current implementation) we might consider an object with all the field that are described in proj4js:
https://github.com/proj4js/proj4js/blob/master/lib/Proj.js

Right now it can be a "named" protection to simplify things.
We should consider aligning the names to proj specifications in this early stage. Just a thought.

@birkskyum
Copy link
Member Author

birkskyum commented Nov 5, 2024

@HarelM , the return type is a ProjectionTransition for (interpolate-projection, renamed it) and Projection for (step, string) in the style spec implementation. It's just their toString() value I showed. Is that what you meant?

src/expression/types.ts Outdated Show resolved Hide resolved
src/expression/types.ts Outdated Show resolved Hide resolved
src/util/interpolate.test.ts Outdated Show resolved Hide resolved
src/util/interpolate.ts Outdated Show resolved Hide resolved
@@ -170,6 +173,10 @@ class Interpolate implements Expression {

const outputLower = outputs[index].evaluate(ctx);
const outputUpper = outputs[index + 1].evaluate(ctx);

if (this.type.kind == 'projection') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can be added to the below switch-case?

Copy link
Member Author

@birkskyum birkskyum Nov 13, 2024

Choose a reason for hiding this comment

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

A type guard on this.type.kind !== 'projection' is needed, so it turns the switch into this which I think looked worse:

        switch (this.operator) {
            case 'interpolate':
                if (this.type.kind !== 'projection') {
                    return interpolate[this.type.kind](outputLower, outputUpper, t);
                }
            case 'interpolate-hcl':
                return interpolate.color(outputLower, outputUpper, t, 'hcl');
            case 'interpolate-lab':
                return interpolate.color(outputLower, outputUpper, t, 'lab');
            case 'interpolate-projection': {
                return interpolate.projection(outputLower, outputUpper, t);
            }
        }
    ```

Copy link
Member Author

@birkskyum birkskyum Nov 13, 2024

Choose a reason for hiding this comment

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

This is what I'm dealing with

Screenshot 2024-11-13 at 08 44 34

It's been a problem since 'projection' was added as a possible value to .kind:
Screenshot 2024-11-13 at 08 45 18

Copy link
Member Author

Choose a reason for hiding this comment

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

The stupid part is that this, or insertion of any of the other possible string values for kind, doesn't have a type problem

Screenshot 2024-11-13 at 08 51 59

Copy link
Member Author

Choose a reason for hiding this comment

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

case 'interpolate':
      return interpolate[this.type.kind.toString()](outputLower, outputUpper, t);
  case 'interpolate-hcl':
      return interpolate.color(outputLower, outputUpper, t, 'hcl');

A kind.toString() resolves it

Copy link
Collaborator

@HarelM HarelM Nov 13, 2024

Choose a reason for hiding this comment

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

I think this removes the type guard from the interpolate object, which I'm not sure is what we want.
I generally try to avoid using object[property] as much as possible due to these limitations and the fact that it's harder to read the code this way.

Copy link
Member Author

@birkskyum birkskyum Nov 13, 2024

Choose a reason for hiding this comment

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

This is an option too, that doesn't ignore any types

        switch (this.operator) {
            case 'interpolate':
                switch (this.type.kind) {
                    case 'number':
                        return interpolate.number(outputLower, outputUpper, t);
                    case 'color':
                        return interpolate.color(outputLower, outputUpper, t, 'rgb');
                    case 'array':
                        return interpolate.array(outputLower, outputUpper, t);
                    case 'padding':
                        return interpolate.padding(outputLower, outputUpper, t);
                    case 'variableAnchorOffsetCollection':
                        return interpolate.variableAnchorOffsetCollection(outputLower, outputUpper, t);
                }
            case 'interpolate-hcl':
                return interpolate.color(outputLower, outputUpper, t, 'hcl');
            case 'interpolate-lab':
                return interpolate.color(outputLower, outputUpper, t, 'lab');
            case 'interpolate-projection': {
                return interpolate.projection(outputLower, outputUpper, t);
            }
        }

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like this one better to be honest.

Copy link
Collaborator

Choose a reason for hiding this comment

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

By the way, if this is possible for other types, why do we need an extra case for interpolate-projection then?

@@ -180,6 +181,8 @@ function typeToMarkdownLink(type: string): string {
case 'paint':
case 'layout':
return ` [${type}](layers.md#${type.toLocaleLowerCase()})`;
case 'projectionconfig':
return ' [projectionConfig](projection.md)';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this really needed here?
This is used for the docs to allow link to an object type related to the expressions I think, and I'm not sure projectionConfig can be a return type of an expression, right?

Copy link
Member Author

@birkskyum birkskyum Nov 13, 2024

Choose a reason for hiding this comment

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

This is an overwrite I made so that the url of the main Projection page doesn't get a /projectionConfig url, which I thought was undesirable, compared to staying with /projection. I can let it generate the /projectionConfig if preferred.

Screenshot 2024-11-13 at 09 04 55

This is the warning I got on mkdocs-build, when it makes the root pages from the v8, before I added it:
WARNING - Doc file 'root.md' contains a link 'projectionConfig.md', but the target is not found among documentation files.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, I see...
I'm trying to think if there's a way to solve this elegantly, but I can't think of a good idea at the moment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this is a good idea, but in proj4js they use the term ProjectionDefinition for the values related to the projection:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/proj4/index.d.ts

I'm not sure what I think about it though - this is for the "internal" type, the one used in the expressions, not the root projection type.

@HarelM
Copy link
Collaborator

HarelM commented Nov 13, 2024

This looks great!!! thanks!
I added a few minor comments, otherwise approved.

@HarelM
Copy link
Collaborator

HarelM commented Nov 13, 2024

Can you create a branch in this repo and merge this PR to this new branch so I'll see if I can try out some things in order to solve the parts that still annoys me?

@HarelM
Copy link
Collaborator

HarelM commented Nov 13, 2024

Please also checkout the code coverage report, there are other stuff that are not covered there, which we need to think how to address, maybe out of scope for this PR.
But for example interpolateFactory is not used, maybe we should remove it instead of adding more logic to it...

@birkskyum birkskyum changed the base branch from main to projection-expression-collecting-branch November 13, 2024 08:39
@birkskyum
Copy link
Member Author

birkskyum commented Nov 13, 2024

I've made a projection-expression-collecting-branch in this repo, and changed the base branch of this PR to it - is that what you meant?

@birkskyum birkskyum merged commit 107bd0e into maplibre:projection-expression-collecting-branch Nov 13, 2024
6 checks passed
@HarelM
Copy link
Collaborator

HarelM commented Nov 13, 2024

Yes, thanks!

birkskyum added a commit that referenced this pull request Nov 13, 2024
birkskyum added a commit that referenced this pull request Nov 15, 2024
birkskyum added a commit that referenced this pull request Nov 15, 2024
birkskyum added a commit that referenced this pull request Nov 16, 2024
birkskyum added a commit that referenced this pull request Nov 17, 2024
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

Successfully merging this pull request may close these issues.

4 participants