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

Fix google map offsets *in* "china" #34

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions plugins/fix-china-map-offset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
// ==UserScript==
// @id iitc-plugin-fix-china-map-offset
// @name IITC plugin: Fix maps offsets in China
// @category Tweaks
// @version 0.2.0.@@DATETIMEVERSION@@
// @description [@@BUILDNAME@@-@@BUILDDATE@@] Show correct maps for China user by applying offset tweaks.
@@METAINFO@@
// ==/UserScript==

@@PLUGINSTART@@

// PLUGIN START ////////////////////////////////////////////////////////

// use own namespace for plugin
window.plugin.fixChinaOffset = {};

// Before understanding how this plugin works, you should know 3 points:
//
// Point1.
// The coordinate system of Ingress is WGS-84.
// However, the tiles of Google maps (except satellite map) and some other maps
// in China have offsets (base on GCJ-02 coordinate system) by China policy.
// That means, if you request map tiles by giving GCJ-02 position, you
// will get the correct map.
//
// Point2.
// Currently there are no easy algorithm to transform from GCJ-02 to WGS-84,
// but we can easily transform data from WGS-84 to GCJ-02.
//
// Point3.
// When using, for example, Google maps in IITC, the layer structure looks like this:
// --------------------------
// | Other Leaflet layers | (Including portals, links, fields, and so on)
// --------------------------
// | L.GridLayer.GoogleMutant | (Only for controling)
// --------------------------
// | Google Map layer | (Generated by Google Map APIs, for rendering maps)
// --------------------------
//
// When users are interacting with L.GridLayer.GoogleMutant (for example, dragging, zooming),
// L.GridLayer.GoogleMutant will perform the same action on the Google Map layer using Google
// Map APIs.
//
// So, here is the internal of the plugin:
//
// The plugin overwrites behaviours of L.GridLayer.GoogleMutant. When users are dragging the map,
// L.GridLayer.GoogleMutant will pass offseted positions to Google Map APIs (WGS-84 to GCJ-02).
// So Google Map APIs will render a correct map.
//
// The offset between Google maps and Ingress objects can also be fixed by applying
// WGS-84 to GCJ-02 transformation on Ingress objects. However we cannot simply know
// the requesting bounds of Ingress objects because we cannot transform GCJ-02 to
// WGS-84. As a result, the Ingress objects on maps would be incomplete.
//
// The algorithm of transforming WGS-84 to GCJ-02 comes from:
// https://on4wp7.codeplex.com/SourceControl/changeset/view/21483#353936
// There is no official algorithm because it is classified information.
//
// Here we use the PRCoords implementation of this algorithm, which contains
// a more careful yet still rough "China area" check in "insane_is_in_china.js".
// Comments and code style have been adapted, mainly to remove profanity.
// https://github.com/Artoria2e5/PRCoords
//
// Correction is made for maps that have the parameter
// needFixChinaOffset: true
// in options. For an example of work, see Leaflet.GoogleMutant.js

plugin.fixChinaOffset.isInGoogle = (function () { // insane_is_in_china
// This set of points roughly illustrates the scope of Google's
// distortion. It has nothing to do with national borders etc.
// Points around Hong Kong/Shenzhen are mapped with a little more precision,
// in hope that it will make agents work a bit more smoothly there.
//
// Edits around these points are welcome.
var POINTS = [
// start hkmo
114.433722, 22.064310,
114.009458, 22.182105,
113.599275, 22.121763,
113.583463, 22.176002,
113.530900, 22.175318,
113.529542, 22.210608,
113.613377, 22.227435,
113.938514, 22.483714,
114.043449, 22.500274,
114.138506, 22.550640,
114.222984, 22.550960,
114.366803, 22.524255,
// end hkmo
115.254019, 20.235733,
121.456316, 26.504442,
123.417261, 30.355685,
124.289197, 39.761103,
126.880509, 41.774504,
127.887261, 41.370015,
128.214602, 41.965359,
129.698745, 42.452788,
130.766139, 42.668534,
131.282487, 45.037051,
133.142361, 44.842986,
134.882453, 48.370596,
132.235531, 47.785403,
130.980075, 47.804860,
130.659026, 48.968383,
127.860252, 50.043973,
125.284310, 53.667091,
120.619316, 53.100485,
119.403751, 50.105903,
117.070862, 49.690388,
115.586019, 47.995542,
118.599613, 47.927785,
118.260771, 46.707335,
113.534759, 44.735134,
112.093739, 45.001999,
111.431259, 43.489381,
105.206324, 41.809510,
96.485703, 42.778692,
94.167961, 44.991668,
91.130430, 45.192938,
90.694601, 47.754437,
87.356293, 49.232005,
85.375791, 48.263928,
85.876055, 47.109272,
82.935423, 47.285727,
81.929808, 45.506317,
79.919457, 45.108122,
79.841455, 42.178752,
73.334917, 40.076332,
73.241805, 39.062331,
79.031902, 34.206413,
78.738395, 31.578004,
80.715812, 30.453822,
81.821692, 30.585965,
85.501663, 28.208463,
92.096061, 27.754241,
94.699781, 29.357171,
96.079442, 29.429559,
98.910308, 27.140660,
97.404057, 24.494701,
99.400021, 23.168966,
100.697449, 21.475914,
102.976870, 22.616482,
105.476997, 23.244292,
108.565621, 20.907735,
107.730505, 18.193406,
110.669856, 17.754550,
];
var lats = POINTS.filter(function (_, idx) { return idx % 2 === 1; });
var lngs = POINTS.filter(function (_, idx) { return idx % 2 === 0; });
POINTS = null; // not needed anyway...

/// *** pnpoly *** ///
// Wm. Franklin's 8-line point-in-polygon C program
// Copyright (c) 1970-2003, Wm. Randolph Franklin
// Copyright (c) 2017, Mingye Wang (js translation)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimers.
// 2. Redistributions in binary form must reproduce the above
// copyright notice in the documentation and/or other materials
// provided with the distribution.
// 3. The name of W. Randolph Franklin may not be used to endorse or
// promote products derived from this Software without specific
// prior written permission.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
var pnpoly = function (xs, ys, x, y) {
if (!(xs.length === ys.length)) { throw new Error('pnpoly: assert(xs.length === ys.length)'); }
var inside = 0;
for (var i = 0, j = xs.length - 1; i < xs.length; j = i++) {
inside ^= ys[i] > y !== ys[j] > y &&
x < (xs[j] - xs[i]) * (y - ys[i]) / (ys[j] - ys[i]) + xs[i];
}
return !!inside;
};
/// ^^^ pnpoly ^^^ ///

var isInGoogle = function (coords) {
// Yank out South China Sea as it's not distorted.
return coords.lat >= 17.754 && coords.lat <= 55.8271 &&
coords.lng >= 72.004 && coords.lng <= 137.8347 &&
pnpoly(lats, lngs, coords.lat, coords.lng);
};

return isInGoogle;
})();

plugin.fixChinaOffset.gcjObfs = (function () {
/// Krasovsky 1940 ellipsoid
/// @const
var GCJ_A = 6378245;
var GCJ_EE = 0.00669342162296594323; // f = 1/298.3; e^2 = 2*f - f**2

var gcjObfs = function (wgs) {

var x = wgs.lng - 105,
y = wgs.lat - 35;
// These distortion functions accept (x = lon - 105, y = lat - 35).
// They return distortions in terms of arc lengths, in meters.
var dLat_m = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)) +
20 / 3 * (
2 * Math.sin(x * 6 * Math.PI) + 2 * Math.sin(x * 2 * Math.PI) +
2 * Math.sin(y * Math.PI) + 4 * Math.sin(y / 3 * Math.PI) +
16 * Math.sin(y / 12 * Math.PI) + 32 * Math.sin(y / 30 * Math.PI));
var dLon_m = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)) +
20 / 3 * (
2 * Math.sin(x * 6 * Math.PI) + 2 * Math.sin(x * 2 * Math.PI) +
2 * Math.sin(x * Math.PI) + 4 * Math.sin(x / 3 * Math.PI) +
15 * Math.sin(x / 12 * Math.PI) + 30 * Math.sin(x / 30 * Math.PI));

var radLat = wgs.lat / 180 * Math.PI;
var magic = 1 - GCJ_EE * Math.pow(Math.sin(radLat), 2); // just a common expr

// Arc lengths per degree, on the wrong ellipsoid
var lat_deg_arclen = Math.PI / 180 * (GCJ_A * (1 - GCJ_EE)) * Math.pow(magic, 1.5);
var lon_deg_arclen = Math.PI / 180 * (GCJ_A * Math.cos(radLat) / Math.sqrt(magic));

return {
lat: wgs.lat + dLat_m / lat_deg_arclen,
lng: wgs.lng + dLon_m / lon_deg_arclen,
};
};

return gcjObfs;
})();

plugin.fixChinaOffset.process = function (wgs) {
return plugin.fixChinaOffset.isInGoogle(wgs)
? plugin.fixChinaOffset.gcjObfs(wgs)
: wgs;
};

///////// begin overwrited L.GridLayer /////////

L.GridLayer.prototype._getTiledPixelBounds = (function () {
return function (center) {
/// modified here ///
center = window.plugin.fixChinaOffset.getLatLng(center, this.options);
/////////////////////
var map = this._map,
mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
scale = map.getZoomScale(mapZoom, this._tileZoom),
pixelCenter = map.project(center, this._tileZoom).floor(),
halfSize = map.getSize().divideBy(scale * 2);

return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
};
})(L.GridLayer.prototype._getTiledPixelBounds);

L.GridLayer.prototype._setZoomTransform = (function (original) {
return function (level, center, zoom) {
center = window.plugin.fixChinaOffset.getLatLng(center, this.options);
original.apply(this, [level, center, zoom]);
};
})(L.GridLayer.prototype._setZoomTransform);

L.GridLayer.GoogleMutant.prototype._update = (function () {
return function () {
this.options.needFixChinaOffset = true;
// zoom level check needs to happen before super's implementation (tile addition/creation)
// otherwise tiles may be missed if maxNativeZoom is not yet correctly determined
if (this._mutant) {
var center = this._map.getCenter();
/// modified here ///
center = window.plugin.fixChinaOffset.getLatLng(center, this.options);
/////////////////////
/* eslint-disable */
var _center = new google.maps.LatLng(center.lat, center.lng);

this._mutant.setCenter(_center);
var zoom = this._map.getZoom();
var fractionalLevel = zoom !== Math.round(zoom);
var mutantZoom = this._mutant.getZoom();

//ignore fractional zoom levels
if (!fractionalLevel && (zoom != mutantZoom)) {
this._mutant.setZoom(zoom);

if (this._mutantIsReady) this._checkZoomLevels();
//else zoom level check will be done later by 'idle' handler
}
/* eslint-enable */
}

L.GridLayer.prototype._update.call(this);
};
})(L.GridLayer.GoogleMutant.prototype._update);

window.plugin.fixChinaOffset.getLatLng = function (pos, options) {
// No offsets in satellite and hybrid maps
if (options.needFixChinaOffset === true && options.type !== 'satellite' && options.type !== 'hybrid') {
return plugin.fixChinaOffset.process(pos);
}
return pos;
};

var setup = function () {};

// PLUGIN END //////////////////////////////////////////////////////////

@@PLUGINEND@@
Loading