Skip to content
Merged
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
2 changes: 2 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ en:
dashboard_tooltip: Open the Rapid Plateau progress dashboard (new tab)
zoom_overview_label: Zoom 14
zoom_overview_tooltip: Snap to zoom 14 so tiles stop loading and panning stays fast.<br>Buildings render from z16.
plateau_conflation:
osm_layer_off: "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
height_transfer:
section_title: Plateau tags
additions: Add from Plateau
Expand Down
3 changes: 3 additions & 0 deletions data/l10n/core.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"zoom_overview_label": "Zoom 14",
"zoom_overview_tooltip": "Snap to zoom 14 so tiles stop loading and panning stays fast.<br>Buildings render from z16."
},
"plateau_conflation": {
"osm_layer_off": "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
},
"height_transfer": {
"section_title": "Plateau tags",
"additions": "Add from Plateau",
Expand Down
3 changes: 3 additions & 0 deletions data/l10n/core.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2962,6 +2962,9 @@
"photos": {
"hires": "高解像度"
},
"plateau_conflation": {
"osm_layer_off": "OpenStreetMap のデータのレイヤーが消えているあいだは、Plateau の建物を表示しません。既存の建物と重なるかを確かめられないためです。Shift+O で戻せます。"
},
"preferences": {
"color_selection": {
"default": "既定"
Expand Down
60 changes: 60 additions & 0 deletions modules/services/PlateauService.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export class PlateauService extends AbstractSystem {
rejected: new Set() // Set(entityID) - overlapping with OSM
};

// OSM のレイヤーが消えている件を伝えたかどうか。レイヤーが戻ると false に戻す。
this._osmLayerOffNotified = false;

// Cache for coverage area GeoJSON (loaded once, used by PixiLayerPlateauCoverage)
this._coverageData = null; // GeoJSON FeatureCollection or null
this._coveragePromise = null; // Promise<FeatureCollection> when inflight
Expand Down Expand Up @@ -337,13 +340,70 @@ export class PlateauService extends AbstractSystem {
// Client-side conflation: hide Plateau buildings that overlap existing OSM
const useConflationStr = utilStringQs(window.location.hash).plateau_conflation;
if (useConflationStr !== 'false' && useConflationStr !== 'no') {
const missing = this._osmDataMissing();
if (missing) {
if (missing === 'layer-off') this._notifyOsmLayerOff();
return [];
}
entities = this._filterPlateauOverlaps(entities, ds.graph);
}

return entities;
}


/**
* _osmDataMissing
* 重なりの判定は、編集ソフトの中にある OSM の建物だけを材料にする。
* 材料が集まっていない状態では「OSM に無い建物」と「まだ確かめられていない建物」を
* 区別できない。区別しないまま候補を出すと、すでに OSM にある建物を重ねて
* 登録することになるため、そのときは候補を出さない。
*
* OSM のレイヤーを消すと `PixiLayerOsm` の描画が先頭で止まり、その先の
* `context.loadTiles()` に届かない。画面から消えるだけでなく、編集ソフトの中身も
* 空のままになる。
*
* @return {string?} 材料が揃っていない理由。'layer-off' か 'tiles'。揃っていれば null
*/
_osmDataMissing() {
const layer = this.context.systems.gfx?.scene?.layers?.get('osm');
if (layer && layer.enabled === false) return 'layer-off';
this._osmLayerOffNotified = false;

// タイルの取得に失敗したまま再取得されない経路もあるため、取得済みかどうかも見る。
// 取得済みの一覧は上流のファイルの持ち物で、上流を取り込んだときに形が変わりうる。
// 読めないときは判断せず、これまでどおり判定に進む。
const loaded = this.context.services?.osm?._tileCache?.loaded;
if (!(loaded instanceof Set)) return null;

const tiles = this._tiler.getTiles(this.context.viewport).tiles;
if (!tiles.length) return null;

return tiles.some(tile => !loaded.has(tile.id)) ? 'tiles' : null;
}


/**
* _notifyOsmLayerOff
* 候補が出ない理由を利用者に伝える。
* レイヤーが消えたままなのは利用者が直せる状態なので伝える。
* タイルの取得は待てば終わるので伝えない。
* 同じ状態が続くあいだは一度だけ出し、レイヤーが戻ったときに出し直せるようにする。
*/
_notifyOsmLayerOff() {
if (this._osmLayerOffNotified) return;
this._osmLayerOffNotified = true;

const flash = this.context.systems.ui?.Flash;
if (typeof flash !== 'function') return;

const l10n = this.context.systems.l10n;
const key = 'plateau_conflation.osm_layer_off';
flash.duration(5000).label(l10n ? l10n.t(key) : key);
flash();
}


/**
* loadTiles
* Schedule any data requests needed to cover the current map view
Expand Down
128 changes: 128 additions & 0 deletions test/browser/services/PlateauService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,134 @@ describe('PlateauService', () => {
});


describe('#getData の材料の確認', () => {
// 判定は編集ソフトの中にある OSM の建物だけを材料にする。
// 材料が集まらない状態では、重なりが無いのか確かめられていないのかを
// 区別できない。区別できないまま候補を出すと、すでに OSM にある建物を
// 重ねて登録することになる。

// 表示範囲を覆うタイルの id を、判定と同じ計算で求める
function tileIDsInView(service) {
return service._tiler.getTiles(service.context.viewport).tiles.map(t => t.id);
}

function setupDataset(coords) {
const base = new Rapid.Graph();
const tree = new Rapid.Tree(base); // 空の graph から作り、差分で登録させる
let graph = base;
const nodeIds = [];
for (let i = 0; i < coords.length; i++) {
const nodeId = 'pgd-n' + i;
nodeIds.push(nodeId);
graph = graph.replace(Rapid.osmNode({ id: nodeId, loc: coords[i] }));
}
nodeIds.push(nodeIds[0]);
const way = Rapid.osmWay({ id: 'pgdWay', nodes: nodeIds, tags: { building: 'yes' } });
graph = graph.replace(way);
_service._datasets.ds1 = { id: 'ds1', graph, tree, cache: {}, lastv: null };
return way;
}

function setOsmState(service, { layerEnabled = true, tilesLoaded = true } = {}) {
const ctx = service.context;
const loaded = new Set(tilesLoaded ? tileIDsInView(service) : []);
ctx.services = { osm: { _tileCache: { loaded: loaded } } };
ctx.systems.gfx.scene = { layers: new Map([['osm', { id: 'osm', enabled: layerEnabled }]]) };
}

beforeEach(() => {
const c = _service.context.viewport.visibleExtent().center();
setupDataset([
[c[0] - 0.0001, c[1] - 0.0001], [c[0] + 0.0001, c[1] - 0.0001],
[c[0] + 0.0001, c[1] + 0.0001], [c[0] - 0.0001, c[1] + 0.0001]
]);
});

it('sanity: the test dataset yields the building', () => {
setOsmState(_service, {});
const ways = _service.getData('ds1', { skipConflation: true }).filter(e => e.type === 'way');
expect(ways).to.have.lengthOf(1, '下ごしらえが効いている');
});

it('returns no candidates while the OSM layer is switched off', () => {
setOsmState(_service, { layerEnabled: false });
expect(_service.getData('ds1')).to.have.lengthOf(0, 'OSM のレイヤーが消えている');
});

it('returns no candidates while the OSM tiles covering the view are not loaded', () => {
setOsmState(_service, { tilesLoaded: false });
expect(_service.getData('ds1')).to.have.lengthOf(0, 'タイルが未取得');
});

it('returns candidates once the layer is on and the tiles are loaded', () => {
setOsmState(_service, {});
const ways = _service.getData('ds1').filter(e => e.type === 'way');
expect(ways).to.have.lengthOf(1, '材料が揃っている');
});

it('still returns everything for the height transfer path', () => {
// 高さの転記は、OSM の建物と重なる PLATEAU 建物を必要とする。
// 材料の有無で結果を変えない。
setOsmState(_service, { layerEnabled: false, tilesLoaded: false });
const ways = _service.getData('ds1', { skipConflation: true }).filter(e => e.type === 'way');
expect(ways).to.have.lengthOf(1);
});

it('judges as before when the OSM state cannot be read', () => {
// 上流の取り込みで持ち物の形が変わったときに、候補が出なくなるのを避ける。
_service.context.services = {};
_service.context.systems.gfx.scene = undefined;
const ways = _service.getData('ds1').filter(e => e.type === 'way');
expect(ways).to.have.lengthOf(1);
});

// 候補が出ない理由を利用者に伝える。
// レイヤーを消したままなのは利用者が直せる状態なので伝える。
// タイルの取得は待てば終わるので伝えない。
function mockFlash() {
const f = () => { f.calls.push(f._label); return f; };
f.calls = [];
f.duration = () => f;
f.label = (t) => { f._label = t; return f; };
return f;
}

function withUi(service) {
const flash = mockFlash();
service.context.systems.ui = { Flash: flash };
service.context.systems.l10n = { t: (k) => k };
return flash;
}

it('tells the user once while the OSM layer stays switched off', () => {
const flash = withUi(_service);
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
_service.getData('ds1');
expect(flash.calls).to.have.lengthOf(1, '同じ状態で何度も出さない');
expect(flash.calls[0]).to.equal('plateau_conflation.osm_layer_off');
});

it('stays quiet while the tiles are still loading', () => {
const flash = withUi(_service);
setOsmState(_service, { tilesLoaded: false });
_service.getData('ds1');
expect(flash.calls).to.have.lengthOf(0);
});

it('tells the user again after the layer is switched on and off', () => {
const flash = withUi(_service);
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
setOsmState(_service, { layerEnabled: true });
_service.getData('ds1');
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
expect(flash.calls).to.have.lengthOf(2);
});
});


describe('#_checkWayOverlapsOsmBuildings', () => {
function makePlateauWay(graph, wayId, coords) {
const nodeIds = [];
Expand Down