Compare commits

..
4 Commits
Author SHA1 Message Date
raucao 0f6272006c 1.34.4
CI / Lint (push) Successful in 1m8s
CI / Test (push) Successful in 1m27s
2026-09-24 19:08:53 +02:00
raucao 85de25032e Merge pull request 'Add operator info to place details' (#111) from feature/operator into master
CI / Lint (push) Successful in 1m3s
CI / Test (push) Successful in 1m22s
Reviewed-on: #111
2026-09-24 17:07:24 +00:00
raucao b9103b28a9 Add operator info to place details
CI / Lint (pull_request) Successful in 1m5s
CI / Test (pull_request) Successful in 1m23s
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2026-09-24 19:02:27 +02:00
raucao b8a9c71657 Update README
CI / Lint (push) Successful in 1m3s
CI / Test (push) Successful in 1m21s
2026-09-21 12:09:38 +02:00
9 changed files with 224 additions and 7 deletions
+6 -2
View File
@@ -8,8 +8,11 @@
<br>
Marco (as in [Marco Polo][1]) is an unhosted maps application that respects
your privacy and choices. It allows users to connect their own remote storage
to sync place bookmarks across apps and devices.
your privacy and choices.
It allows users to connect their own remote storage to sync place bookmarks
across apps and devices. It also supports the decentralized sharing of place
photos (and soon reviews) via [Nostr][2].
This README outlines the details of collaborating on this Ember application, or
just building and deploying it for yourself.
@@ -79,3 +82,4 @@ To run the script, you need `imagemagick` and `librsvg` installed:
- [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
[1]: https://en.wikipedia.org/wiki/Marco_Polo
[2]: https://nostr.org
+103
View File
@@ -316,6 +316,70 @@ export default class PlaceDetails extends Component {
return this.formatMultiLine(val);
}
get operatorNames() {
const raw = this.tags.operator;
if (!raw) return null;
const values = raw
.split(';')
.map((value) => value.trim())
.filter(
(value) =>
value && value.toLowerCase() !== 'yes' && value.toLowerCase() !== 'no'
);
if (values.length === 0) return null;
return this.formatMultiLine(values.join(';'));
}
get operatorWebsite() {
const val = this.tags['operator:website'];
return val ? val.trim() : null;
}
get operatorWebsiteLabel() {
return this.operatorWebsite ? this.getDomain(this.operatorWebsite) : null;
}
get operatorWikidata() {
const val = this.tags['operator:wikidata'];
if (!val) return null;
return (
val
.split(';')
.map((v) => v.trim())
.filter(Boolean)[0] || null
);
}
get operatorWikipedia() {
const val = this.tags['operator:wikipedia'];
if (!val) return null;
return (
val
.split(';')
.map((v) => v.trim())
.filter(Boolean)[0] || null
);
}
get operatorWikipediaUrl() {
const val = this.operatorWikipedia;
if (!val) return null;
const match = val.match(/^([a-z-]+):(.+)$/i);
if (match) {
return `https://${match[1]}.wikipedia.org/wiki/${match[2]}`;
}
return `https://wikipedia.org/wiki/${val}`;
}
get showOperator() {
return !!(
this.operatorNames ||
this.operatorWebsite ||
this.operatorWikidata ||
this.operatorWikipedia
);
}
get cuisine() {
if (!this.tags.cuisine) return null;
return this.tags.cuisine
@@ -490,6 +554,45 @@ export default class PlaceDetails extends Component {
</p>
{{/if}}
{{#if this.showOperator}}
<p class="content-with-icon">
<Icon @name="briefcase" @title="Operator" />
<span>
{{this.operatorNames}}
{{#if this.operatorWebsite}}
<br />
<a
href={{this.operatorWebsite}}
target="_blank"
rel="noopener noreferrer"
>
{{this.operatorWebsiteLabel}}
</a>
{{/if}}
{{#if this.operatorWikidata}}
<br />
<a
href="https://www.wikidata.org/wiki/{{this.operatorWikidata}}"
target="_blank"
rel="noopener noreferrer"
>
Wikidata
</a>
{{/if}}
{{#if this.operatorWikipedia}}
<br />
<a
href={{this.operatorWikipediaUrl}}
target="_blank"
rel="noopener noreferrer"
>
Wikipedia
</a>
{{/if}}
</span>
</p>
{{/if}}
{{#if this.phone}}
<p class="content-with-icon">
<Icon @name="phone" @title="Phone" />
+2
View File
@@ -5,6 +5,7 @@
import activity from 'feather-icons/dist/icons/activity.svg?raw';
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
import briefcase from 'feather-icons/dist/icons/briefcase.svg?raw';
import featherCamera from 'feather-icons/dist/icons/camera.svg?raw';
import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
import chevronLeft from 'feather-icons/dist/icons/chevron-left.svg?raw';
@@ -182,6 +183,7 @@ const ICONS = {
'boxing-glove-up': boxingGloveUp,
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
bridge,
briefcase,
bus,
camera,
'feather-camera': featherCamera,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.34.3",
"version": "1.34.4",
"private": true,
"description": "Unhosted maps app",
"repository": {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -42,7 +42,7 @@
<meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-CZdZiL1p.js"></script>
<script type="module" crossorigin src="/assets/main-C9gFC_z2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-Bv3zmRKA.css">
</head>
<body>
@@ -457,4 +457,112 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom('.place-payment-methods').doesNotExist();
});
test('it renders the operator name with a briefcase icon', async function (assert) {
const place = {
title: 'Operated Cafe',
osmTags: {
operator: 'Le Méridien',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const operatorBlock = this.element.querySelector(
'.meta-info .content-with-icon span.icon[title="Operator"]'
);
assert.ok(operatorBlock, 'Operator block is rendered');
const row = operatorBlock.closest('.content-with-icon');
assert.dom(row).includesText('Le Méridien');
});
test('it splits semicolon-separated operators onto separate lines', async function (assert) {
const place = {
title: 'Multi Operator Stop',
osmTags: {
operator: 'De Lijn;TEC',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const row = this.element
.querySelector('span.icon[title="Operator"]')
.closest('.content-with-icon');
assert.dom(row).includesText('De Lijn');
assert.dom(row).includesText('TEC');
assert.true(
row.querySelector('span:not(.icon)').innerHTML.includes('<br>'),
'Operators are separated by a line break'
);
});
test('it suppresses invalid operator values of yes and no', async function (assert) {
const place = {
title: 'Invalid Operator',
osmTags: {
operator: 'yes',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
assert
.dom('span.icon[title="Operator"]')
.doesNotExist('Operator block is hidden for operator=yes');
});
test('it renders operator wikidata and website links', async function (assert) {
const place = {
title: 'Linked Operator',
osmTags: {
operator: 'Flixbus',
'operator:wikidata': 'Q15712278',
'operator:website': 'https://www.flixbus.com/',
'operator:wikipedia': 'en:FlixBus',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
const row = this.element
.querySelector('span.icon[title="Operator"]')
.closest('.content-with-icon');
const wikidataLink = row.querySelector(
'a[href="https://www.wikidata.org/wiki/Q15712278"]'
);
assert.ok(wikidataLink, 'Wikidata link is rendered');
assert.dom(wikidataLink).hasText('Wikidata');
const websiteLink = row.querySelector('a[href="https://www.flixbus.com/"]');
assert.ok(websiteLink, 'Operator website link is rendered');
assert.dom(websiteLink).hasText('www.flixbus.com');
const wikipediaLink = row.querySelector(
'a[href="https://en.wikipedia.org/wiki/FlixBus"]'
);
assert.ok(wikipediaLink, 'Operator Wikipedia link is rendered');
assert.dom(wikipediaLink).hasText('Wikipedia');
});
test('it renders the operator block when only operator subkeys are present', async function (assert) {
const place = {
title: 'Subkey Only',
osmTags: {
'operator:wikidata': 'Q42',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
assert
.dom('span.icon[title="Operator"]')
.exists('Operator block is rendered from subkeys alone');
assert
.dom('a[href="https://www.wikidata.org/wiki/Q42"]')
.exists('Wikidata link is rendered');
});
});