albar965 / littlenavmap

Little Navmap is a free flight planner, navigation tool, moving map, airport search and airport information system for Flight Simulator X, Microsoft Flight Simulator 2020, Prepar3D and X-Plane.
https://albar965.github.io/littlenavmap.html
GNU General Public License v3.0
1.23k stars 161 forks source link

Feature/future web 1 #1153

Closed u-an-i closed 2 weeks ago

u-an-i commented 2 weeks ago

@albar965

Creation, Documentation of plugin configuration JSON and Migration of autozoom and ol-maps plugin is done.

Still open: plugin toolbar styling adjustment with new communication protocoll between plugin and plugin host, migration of example plugins.

Also done is new communication protocol which uses technology required with fully sandboxed iframes. Unfortunately just in the last hour from now after 4 nights of work the full sandbox turned out to be disallowing too much when I migrated ol-map and the next step down is where we were before. I remain on this new protocol though now.

A Draft PR here for your previewing pleasure.

u-an-i commented 2 weeks ago

@albar965 done

albar965 commented 2 weeks ago

Is the problem with the iframes solved?

Or, to put is short: Good enough for 3.0.7.rc1? :wink:

u-an-i commented 2 weeks ago

@albar965 yes.

I allow as much as before now. From the beginning, restrictions were intended to be enforced but web browsers don't support the method chosen then. Now restrictions could be enforced but I assume most meaningful plugins would require them to be lifted.
Lifting can be granted by the user but I did't want to have the user be bothered by most plugins although I store permissions granted in the session storage so that per session only 1 "nag" would occur. Let me know if you like restrictions to be enforced.
The consequence of the current lenient approach is any enabled plugin could do anything in the web frontend, only the webpage itself is affected.

albar965 commented 2 weeks ago

Thanks. Fine with me.

u-an-i commented 2 weeks ago

@albar965 , to me occurred: if we now enable max. shielding, plugin devs can take it into account now and wouldn't have to rework their plugin init if we ever decide to enforce that shielding.

The nag question to grant acess is a sligth nuisance but the map and the menu = the whole "lnm web frontend" is 100% safe from any plugin error - except if the user granted access -- but that is only valid for the session.
If almost all plugins like to have accees and the user grants them that user is at risk as if we didn't shield - plus he was asked a question. But perhaps many plugin scenarios exist where the plugin does not need acceess.

What do you think ?

albar965 commented 1 week ago

to me occurred: if we now enable max. shielding, plugin devs can take it into account now and wouldn't have to rework their plugin init if we ever decide to enforce that shielding.

The nag question to grant acess is a sligth nuisance but the map and the menu = the whole "lnm web frontend" is 100% safe from any plugin error - except if the user granted access -- but that is only valid for the session. If almost all plugins like to have accees and the user grants them that user is at risk as if we didn't shield - plus he was asked a question. But perhaps many plugin scenarios exist where the plugin does not need acceess.

Not sure if I understand. Will the users see a nag dialog with the current plugins? What are the consequences of low shielding? Privacy leaks? Plugins have access to the full website?

I'm fine with shielding if a future plugin developer can avoid the nag dialog and the current plugins do not show one.

Alex

u-an-i commented 1 week ago

ah :)

the web frontend is a web page, shipped with LNM.

"Plugins" to the web frontend are also webpages but potentially offered by 3rd parties.

To embed web pages in web pages the following methods exist:

Usual web element: disadvantages: CSS from main can affect the embed, CSS from the embed can affect main and other embeds which unknowingly used same selectors. broken embed HTML will brake main page. JavaScript cannot be removed (JS once added to a window can not be removed in current browsers).

iframe:
advantages: none of the disadvantages of usual web elements.
note: JavaScript can access the main page and change things there. This has to be explicitly coded and cannot occur "on its own". This is an intended use case and the plugin gets communicated the current "map HTML version" so that it can act on if it is different from what the developer was working with when writing his code. This is documented in the plugin documentation for developers and has been the case since the beginning.

fully sandboxed iframe:
note: JavaScript cannot access the main page and cannot change things there. It can neither access things in iframes of its own if it has such which is unsensible imo, the ol-map wouldn't work for example.

The config.json contains an entry with which the plugin developer can indicate his plugin likes to access the main page. When that is filled, a nag dialog occurrs. The autozoom plugin shows this nag dialog. Nag dialogs are shown once per session ie. again after browser close if the browser is not configured to restore sessions after close which eg Chrome is not by default as far as I am aware.

If the config.json entry is not filled, I currently do not enable full shielding (but only partial shielding) so any plugin can access the main page. A filled entry and thus a dialogue currently is only a politeness by the plugin developer. However I see this as being changeable in the future when browsers implement finer-grained security attributes, so plugin developers are advised to use the entry when they are accessing the main page.

The reason for not enabling full shielding is the note under "fully sandboxed iframe" above. It's too restrictive and I didn't want every plugin developer to request access to the main page and thus a user see a nag dialog with potentially every plugin because I didn't fathom a plugin which can do meaningful work with full shielding enabled. Now I think perhaps a lot of plugins can work with full shielding.

The only consequence of low shielding is any plugin can have code to access the main page (and in turn anything the main page contains such as the iframes containing the other plugins and pages such as flight plan) which is let run by the web browser and thus change the experience of it for the user during runtime, not permanently -- unexpectedly for the user because no nag dialog occurred. But that code had to be explicitly written by the plugin developer, it cannot be an oversight.

Privacy leaks etc. "cannot" occur, plugins are only a web page and depend on the web browser to work (and the web browser to provide access to any data if this is possible and then the web browser to hopefully show a dialog to the user).

The nag dialog is coded by me in the plugin interface code. It can be avoided like derivable above when the plugin has the appropriate config.json entry not filled. The question is: how many plugins are you fine with showing the dialog? If "none", we leave it as is (the plugin developer should nevertheless be honest and correct and fill the entry when his plugin actually accesses the main page). If "most" I will make full shielding the default and add the entry to the ol-map config.json (and adjust its initialisation code to take into account it might not have required "access"). The autozoom plugin shows the dialog. It needs access, it changes the map zoom. I think having the possibility of a nag dialog is fair to the user: he is then informed and can make a decision whether he trusts the plugin developer enough.

albar965 commented 1 week ago

Thanks for the explanation.

The question is: how many plugins are you fine with showing the dialog?

None.

I'd vote to low shielding. The nagging dialog seems to be unnecessary IMO. I'd expect plugins to access the whole page anyway.

I like the click to center the map much better but in some cases like high zoom distances it jumps to the wrong place. I think this can be fixed later. Dealing with the Mercator projection is not so easy.

I find the dropbox with hide all whole-map-not-covering-plugins' andDynamic touch and drag' map confusing. Why not simply switch between OL and the other map? Or hide the dropbox? Why not put the OL map as another map style instead of a plugin?

u-an-i commented 1 week ago

Thanks for the explanation.

The question is: how many plugins are you fine with showing the dialog?

None.

I'd vote to low shielding. The nagging dialog seems to be unnecessary IMO. I'd expect plugins to access the whole page anyway.

Fair enough. I remove the nagging dialog. I leave the indication entry in the config.json, perhaps we can make use of it one time. I'll update the documentation accordingly.

I like the click to center the map much better but in some cases like high zoom distances it jumps to the wrong place. I think this can be fixed later. Dealing with the Mercator projection is not so easy.

Yes, high zoom distance is an issue, I believe it's when the map gets repeated and thus the bounding box is "wrong". Centering is not so useful then. I'll make it not try to center on high distances.

I find the dropbox with hide all whole-map-not-covering-plugins' andDynamic touch and drag' map confusing. Why not simply switch between OL and the other map? Or hide the dropbox? Why not put the OL map as another map style instead of a plugin?

The dropdown allows to switch between different "exclusive" plugins. "Exclusive" plugins cover the whole viewport ie. only 1 can ever be seen at a time. I can move the ol-map from being a plugin to being an option on the current map's toolbar. But: then "plugins" would let assume they also work for the ol-map ie. every out of the box option on the toolbar. This is currently not the case. Autozoom eg. does not work with ol-map. When both are plugins this is relatable for a user I guess that both act on the main feature and not necessarily on each other.
Once we have a "map api" and/or ol-map is the default map, I would make Autozoom work with ol-map of course.

albar965 commented 1 week ago

Yes, high zoom distance is an issue, I believe it's when the map gets repeated and thus the bounding box is "wrong". Centering is not so useful then. I'll make it not try to center on high distances.

Marble is a nightmare in this regard in Mercator. I have to find the nearest or at least an one x coordinate from a list they return: https://github.com/albar965/littlenavmap/blob/7c1709ab47dba3fdd2c8dfd412a5e03ab9198e95/src/common/coordinateconverter.cpp#L371-L424

u-an-i commented 1 week ago

btw.: there is a geoCoordinates method in Marble's AbstractProjection which is the source basis for GeoDataLatLonBox and in case of the MercatorProjection I believe it's delivering the wrong north/south latitude. The values delivered by LNM appeared to work thus I didn't change anything:

        const int halfImageHeight    = viewport->height() / 2;
        const int yCenterOffset = (int)( asinh( tan( centerLat ) ) * rad2Pixel  );
        const int yTop          = halfImageHeight - 2 * radius + yCenterOffset;
        const int yBottom       = yTop + 4 * radius;
        if ( y >= yTop && y < yBottom ) {
            lat = gd( ( ( halfImageHeight + yCenterOffset ) - y)
                              * pixel2Rad );

            if ( unit == GeoDataCoordinates::Degree ) {
                lat *= RAD2DEG;
            }

            return true; // lat successfully calculated
        }

marble\src\lib\marble\projections\MercatorProjection.cpp:205

halfImageHeight + yCenterOffset is a yMiddle if I followed correctly and y in the case of the viewLatLonAltBox is passed as 0 by Marble (as being the north border of the viewport). Thus Marble delivers the latitude of the vertical center of the viewport instead of the latitude of the vertical northern border of the viewport.

albar965 commented 1 week ago

MercatorProjection::geoCoordinates and SphericalProjection::geoCoordinates is definitely used by LNM. Not sure if I understand what's going on there. So far I did not run into wrong lat values.

u-an-i commented 1 week ago

I also wondered about that no false latitudes are apparent.

Here's the call to the method and the full code:

const GeoDataLatLonAltBox& ViewportParams::viewLatLonAltBox() const
{
    if (d->m_dirtyBox) {
        d->m_viewLatLonAltBox = d->m_currentProjection->latLonAltBox( QRect( QPoint( 0, 0 ),
                        d->m_size ),
                        this );
        d->m_dirtyBox = false;
    }

    return d->m_viewLatLonAltBox;
}

marble\src\lib\marble\ViewportParams.cpp:323
called from littlenavmap\src\mapgui\mappaintwidget.cpp:1161


GeoDataLatLonAltBox MercatorProjection::latLonAltBox( const QRect& screenRect,
                                                      const ViewportParams *viewport ) const
{
    qreal west;
    qreal north = 85*DEG2RAD;
    geoCoordinates( screenRect.left(), screenRect.top(), viewport, west, north, GeoDataCoordinates::Radian );

you can see a Rect of 0,0,width,height is passed to latLonAltBox and 0,0 are passed as x,y to geoCoordinates.

Thus geoCoordinates appears to translate x,y as offset relative to the viewport's lon/lat.


bool MercatorProjection::geoCoordinates( const int x, const int y,
                                         const ViewportParams *viewport,
                                         qreal& lon, qreal& lat,
                                         GeoDataCoordinates::Unit unit ) const
{
    const int radius = viewport->radius();
    Q_ASSERT( radius > 0 );

    // Calculate translation of center point
    const qreal centerLon = viewport->centerLongitude();
    const qreal centerLat = viewport->centerLatitude();

My last statement is corrobated here: the viewport's center lon/lat is gotten.

    // Calculate how many pixel are being represented per radians.
    const float rad2Pixel = (qreal)( 2 * radius )/M_PI;
    const qreal pixel2Rad = M_PI / (2 * radius);

radius as far as I understand -- it's not documented in the source -- is the angle range of 90° on a flat surface when angles are equidistant each other in number of pixels for a given zoom level. Thus 180° rad2Pixel = 2 radius. rad2Pixel and pixel2Rad translate between length in number of pixels of a flat surface and equidistant angles spanned by that surface. They translate a range = delta, not an absolute position.

    {
        const int halfImageWidth = viewport->width() / 2;
        const int xPixels = x - halfImageWidth;
        lon = xPixels * pixel2Rad + centerLon;

The straightforward statement is: lon = centerLon + (-halfImageWidth + x) * pixel2Rad. So from the center first going back to the left viewport border and then right according to the number of px = x given, all translated to angles. On a rectangular map longitudes are the same equidistant each other for all latitudes thus the range = delta of (-halfImageWidth + x) can simply be translated using pixel2Rad. Furthermore, x apparently is the offset from the left viewport border.

        while ( lon > M_PI )  lon -= 2*M_PI;
        while ( lon < -M_PI ) lon += 2*M_PI;

        if ( unit == GeoDataCoordinates::Degree ) {
            lon *= RAD2DEG;
        }
    }

    {
        const int halfImageHeight    = viewport->height() / 2;
        const int yCenterOffset = (int)( asinh( tan( centerLat ) ) * rad2Pixel  );

latitudes in mercator projection are not equidistant each other. Mercator projection's latitude gaps can be derived the following way: the goal is local scale-correctness, this means width and height (height as length in latitude-changing direction along constant longitude) around a given point when small enough should have the same scale factor on the projected map as on a sphere surface. A rectangular map as the projection has every latitude be the same width as the rectangle's width, in particular the same width as the rectangle's center vertical middle's width. The vertical middle's width corresponds to the sphere's equator circumference. For latitudes offset from the equator the circumference of that latitude is smaller however! The offset latitude's circumference is a factor cos(latitude) of the circumference at the equator. image r is the offset latitude's radius, R is the radius at the equator, a circumference is a multiple of 2 * pi of a radius.

The circumference on the sphere which got projected to the width of the rectangle from one offset latitude is different to the circumference on the sphere projected to the width of the rectangle from another offset latitude, following the statement of cos(latitude). If latitudes were equidistant in the projection, heights would be a constant factor scale from the heights on the sphere and the projection wouldn't be scale-correct because the width factor changes. Starting at the projection = the rectangle, each width = longitude difference = longitude delta at a latitude is a factor 1/cos(latitude) of the original, sphere width. Thus each height = latitude difference = latitude delta at a latitude should also be a factor of 1/cos(latitude) of the original, sphere height. Adding up all latitude delta in either north or south direction for delta latitude -> 0 results in the total height in either north or south direction. This is the definite integral of [1/cos(x)]dx from 0 to +-90° , x being latitude. 1/cos(x) is also sec(x). The integral's result is also known as the inverse Gudermannian = inv. gd. See https://en.wikipedia.org/wiki/Integral_of_the_secant_function . Thus equidistant sphere latitudes are projected onto inv. gd. "distributed" rectangle latitudes.
asinh( tan( centerLat ) ) is one expression of the inv. gd.
Thus yCenterOffset begins by translating the sphere latitude to a rectangle (flat surface) latitude. The intention probably is to be in "image space" because images are flat surface rectangles and most map images have their content distributed by the inv. gd along the latitudes. Then the rectangle latitude is converted to pixels. *1

        const int yTop          = halfImageHeight - 2 * radius + yCenterOffset;
        const int yBottom       = yTop + 4 * radius;
        if ( y >= yTop && y < yBottom ) {

observe y, an offset from 0 to image height is pitted against yTop and yBottom. Those latter 2 were likely intended to be the absolute pixel position of the north and south border of the viewport with regard to an image being the whole globe's map. They don't match a relative viewport offset number.
y >= 0 and y < 4radius should and must suffice given yBottom is yTop + 4 radius. radius, remember is the number of pixels for 90° at a zoom level thus 4 * 90 = 360° makes this if condition pointless (x isn't being checked to be >= 0 either).

            lat = gd( ( ( halfImageHeight + yCenterOffset ) - y)
                              * pixel2Rad );

halfImageHeight + yCenterOffset equals yTop + (yBottom - yTop) / 2 which is like a yMiddle. Thus y is offset from yMiddle. Remember x is offset from centerLon - halfImageWidth = the left border.
But this can be reshaped as
lat = gd ( yCenterOffset pixel2Rad + ( halfImageHeight - y ) pixel2Rad ); = *gd ( inv. gd (centerLat) + ( halfImageHeight - y ) pixel2Rad ); The pixel2Rad factor from the inv. gd vanishes and the statement looks more akin the lon statement. The yMiddle is a distraction from the superfluous yTop and yBottom calculation who are not used except in the condition.
Now:
is the last shape a correct calculation of the latitude at a pixel offset y in a viewport on a map whose content is mercator projected, ie. inv. gd distributed along the latitudes ? Yes.**
Note: the expected value by the name of the reference variable passed for the result of y is the value for north, latitudes increase from south to north thus ( +halfImageHeight - y ) is the northern border.

            if ( unit == GeoDataCoordinates::Degree ) {
                lat *= RAD2DEG;
            }

            return true; // lat successfully calculated
        }
    }

    return false; // lat unchanged
}

marble\src\lib\marble\projections\MercatorProjection.cpp:175

*1 Note: the inv. gd translates from numbers being angles to numbers being angles, the target angles' numbers are equidistant like the source angles' numbers are. Only the translated values of the source values on these numbers are not equidistant.

u-an-i commented 1 week ago

writing my last reply took me 4 hours. Only to find everything is fine. But it might be an interesting read.