Jump to content

FieldtypeMeasurement


MarkE
 Share

Recommended Posts

EDIT:
*************
Note that this OP relates to the original version of the module. Version 0.0.19 is now published to the modules library at https://processwire.com/modules/fieldtype-measurement/. As well as many bug fixes and small enhancements, it now also includes an 'in-field' interactive conversion feature and dependent-select in the config. See the readme on the module page for up-to-date details and see this post for a bit of a demo.
*************

This fieldtype and inputfield bundle was built for storing measurement values within a field, rendering them in a variety of formats and converting them to other units or otherwise modifying them via the API.

The API consists of a number of predefined functions, some of which include...

  • render() for rendering the measurement object,
  • valueAs() for converting the value to another unit value,
  • convertTo() for converting the whole measurement object to different units, and
  • add() and subtract() for for modifying the stored value by the value (converted as required) in another measurement.

In the admin the inputfield includes a checkbox (which can be optionally disabled) for converting values on page save. For an example if a value was typed in as centimeters, the unit was changed to metres, and the page saved with this checkbox selected, said value would be automatically converted so that e.g. 170 cm becomes 1.7 m.

measurement-fieldtype-for-processwire.jp

A simple length field using Fieldtype Measurement and Inputfield Measurement.

Combination units (e.g. feet and inches) are also supported.

Please note that this module is 'proof of concept' at the moment - there are limited units available and quite a lot of code tidying to do. More units will be added shortly.

See the GitHub at https://github.com/MetaTunes/FieldtypeMeasurement for full details and updates.

  • Like 7
Link to comment
Share on other sites

Thanks for this contribution @MarkE,

  1. Is there a reason you combine magnitudes into pipe-separated values? This makes it difficult to query. For example, what if I wanted to find values whose magnitude were > 10? Storing these pipe-separated strings make this tricky. I have only quickly looked at the code so you could have already figured this out.
  2. Combination units: I think I get the idea for this (e.g. the feet and inches). From an UX/UI point of view though, maybe it is prone to errors? Editors need to remember to input a pipe as well as remember what side of the pipe is what measurement. Maybe this is an exaggeration, as it is quite easy to remember feet|inches but what if it is another not-so-obvious combination?

The auto-conversation feature looks cool though ?.

Link to comment
Share on other sites

Thanks for the comments @kongondo. As stated, it is POC at the moment so I am very open to suggestions for improvements ? . I have built it with an app in mind, but I wanted to do the fieldtype before proceeding with the app. No doubt using it in anger will result in some changes.

4 minutes ago, kongondo said:

Is there a reason you combine magnitudes into pipe-separated values?

I combined the magnitudes as pipe-separated in order to store them as a string in the DB. I am having second thoughts about this. In fact I am wondering about using arrays and then storing a json in the DB.

At the moment, the queries don't work with combi fields, but there is a wider issue: if there are a mix of units in the field instances then numerical comparisons are meaningless. Therefore it is necessary to loop through the pages with measurements and convertTo() a common unit, then compare - this deals with combi units at the same time (assuming the common unit is single-valued!). I'm therefore thinking how could I solve that wider problem at the same time as the one you mention?

5 minutes ago, kongondo said:

maybe it is prone to errors? Editors need to remember to input a pipe as well as remember what side of the pipe is what measurement.

Fair point. There is some error checking at the moment, but I still need to catch the exceptions which are generated and provide helpful messages. Hopefully the dropdown indicates the format and editors will soon get the hang of this. Again, I am open to better suggestions - it needs a balance between conciseness and idiocy-proof.

Link to comment
Share on other sites

20 minutes ago, MarkE said:

I combined the magnitudes as pipe-separated in order to store them as a string in the DB. I am having second thoughts about this. In fact I am wondering about using arrays and then storing a json in the DB.

Clarification: The above is a slight red herring regarding the isue raised as the multi-values are actually stored in the object as an array. The pipe format is just for the UI and the SQL DB.

Link to comment
Share on other sites

1 hour ago, MarkE said:

I'm therefore thinking how could I solve that wider problem at the same time as the one you mention?

I have learnt (sometimes the hard way) that mixing values  or data types in the database for the sake of UI/UX is never a good a idea. Modelling a real world problem is many times not straightforward. Reading up on DB normalisation helped clarify things for me. Before I design any DB model / schema I ask myself two basic questions:

  1. Will users need to query the data? (e.g. searches, selector)
  2. Will the data require manipulations at the DB level? 

If the answer to any of these questions is YES, I throw out any notions of storing data as JSON. In 90+% of the time this will be the case. The data will need to be queryable. In fact, I rarely store anything as JSON (even with the not-so-recent MySQL JSON-capabilities - the pros and cons of which are a debate for another day, btw). The only thing I store as JSON are things like site settings, data points of a map for a chart, etc., as these don't need querying or direct computation applied on them. In this Fieldtype's case, I would steer away from JSON. The name of the field itself implies mathematical computation will be involved. I would use a DECIMAL data type to store the quantifiable values. You will then be faced with the age-old question of whether to expand horizontally (add more columns to accommodate more combinations of feet|inches- i.e. regular relational design) or expand vertically (add more rows - i.e., Entity Attribute Value [EAV]). Each of these have their pros and cons. 

1 hour ago, MarkE said:

but there is a wider issue: if there are a mix of units in the field instances then numerical comparisons are meaningless. Therefore it is necessary to loop through the pages with measurements and convertTo() a common unit, then compare - this deals with combi units at the same time (assuming the common unit is single-valued!). I'm therefore thinking how could I solve that wider problem at the same time as the one you mention?

Probably one way to do this is to try and ensure that you are comparing apples to apples at the DB-level. You can decide on a base unit that will be used to store all measurement values. For instance, centimetres. In the UI, the users will always see their measurements in the selected unit (feet, inches, etc). You can already see the problem with this ?.  It requires knowledge of the type of measurement that is being stored, e.g. length, volume, area, etc! Does it mean you will have a base unit for all these groups of measurements? What about custom measurements that don't readily fit into one of these paradigms? I am probably overthinking this. Or I have been working on Padloper for too long ?.

1 hour ago, MarkE said:

I have built it with an app in mind, but I wanted to do the fieldtype before proceeding with the app.

Good decision. I think it is 'easier' to bend the UI/UX to fit your DB model than the other way around.

Sorry, I don't have much answers at the moment but hopefully this helps generate a meaningful discussion. 

Link to comment
Share on other sites

19 minutes ago, kongondo said:

You can decide on a base unit that will be used to store all measurement values. For instance, centimetres. In the UI

This was my original approach, which I changed for a reason I can’t recall ?. I’ll reconsider. 
 

Edit: I think the reason was that the base unit definition might change. However, now I’ve got more mileage under my belt with this, I might be able to solve that problem. 

Edited by MarkE
Afterthought
Link to comment
Share on other sites

  • 3 weeks later...

I've put quite a lot of work into this now. There are lots of additional units added plus a greatly extended API. This enhances the formatting capability and introduces 'dimensional analysis'. So it is now possible to (say) multiply measurements and properly interpret the results. - e.g (as a simple example) speed x time = distance

$speed = new Measurement('Speed', 'furlong per fortnight', 20);
$time = new Measurement('Time', 'hour', 3);
$length = $speed->multiplyBy($time, 'Length', 'yard');
d($length->render(['decimals' =>2]));

Result is '39.29yd'

Omitting the 'Length', 'yard' specification means that the result is returned in base units for the first compatible quantity for the computed dimension:

$length = $speed->multiplyBy($time);
d($length->render(['decimals' =>2]));

Result is '35.92m'

See https://github.com/MetaTunes/FieldtypeMeasurement for full details.

The module is still alpha, so use in production systems is not advised. However, the functionality is now pretty complete but I have a bit of tidying up to do - and some more extensive testing in a new app. New units will probably also be added (but users can add their own anyway).

Try it out and let me know of any issues or suggestions ? 

  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

New version 0.0.7 at https://github.com/MetaTunes/FieldtypeMeasurement

This has lots of improvements, including permitting different details in different template contexts of the same field and a much-extended API

So, for example, complex, dimensionally-analysed chains can be called such as in the following (illustrative, albeit rather artificial) script (execute in Tracy console):

$m = $modules->get('FieldtypeMeasurement');
$speed = $m->measurement('Speed', 'furlong per fortnight', 1000);
$time = $m->measurement('Time', 'hour', 3);
$length = $speed->multiplyBy($time);
$length2 = $m->measurement('Length', 'yard', 2);
$area = $length2->power(2);
$length3 = $m->measurement();
$length3->productOf($speed, $time);
$speed2 = $m->measurement('Speed', 'mile per hour', 1);
$speed3 = $m->measurement('Speed')->sumOf($speed2, $speed);
$speed4 = $m->measurement();
$speed4 = $speed2->subtract($speed);
$multi = $m->measurement('Length', 'yard')
	->convertFrom(
		$area->add(
			$length2->subtract(
				$speed4->multiplyBy($time)
			)
				->power(2)
		)->divideBy($length3)
	);
d($multi);

Result is

'magnitude' => 5590.1092160249345
'quantity' => 'Length'
'unit' => 'yard'
'baseUnit' => 'metre'
'dimension' => MetaTunes\MeasurementClasses\Dimension #814
'baseMagnitude' => 5111.5958671332
'shortLabel' => 'yd'

where the dimension object has property dimensionArray ['length' => 1]

This is still alpha, albeit with hopefully fewer bugs. I'll use it in a live app before progressing further.

Link to comment
Share on other sites

  • 3 months later...

New version 0.0.9 at https://github.com/MetaTunes/FieldtypeMeasurement

This has lots of bug fixes, plus a much neater way of doing conversions 'in field' :

  • You can choose to enable the magnitude value to be automatically updated when you change the unit selection in a field - the options are to 'Always' convert, to 'Never' convert or to 'Ask' whether or not to convert each time the unit selection is changed (note that, if conversion is chosen and the unit is changed to 'no selection' then the magnitude value will be blanked).

Many thanks to @kongondoin getting this working via HTMX (and also to @Robin Sfor modifying SelectizeAll). This version has been tested quite extensively, but only in one environment. Different PW versions, modules and your own code may affect it differently. It is therefore not recommended for use in production sites unless you have fully tested it in context first.

  • Like 2
Link to comment
Share on other sites

  • 3 weeks later...

Please note that there is a bug in that the 'in-field' conversion feature is not working properly inside lazy-loaded repeaters. I am looking for a fix for this (any suggestions welcomed ?).

EDIT - The problem can be avoided (for existing repeater items) by disabling dynamic loading, but new items will need to be saved before 'in-field' conversion is enabled.

EDIT 2: Hopefully now all fixed in v0.0.11

Link to comment
Share on other sites

v0.0.12 now available. This fixes a few bugs and also introduces interactive dependent-selects in the config. Now that both the config and the pages have dependent selects, I thought it would be helpful to demo how it all works.

Firstly, the config. On the details tab, you select the quantity you want to measure and then choose what units you want to be selectable within a page (you can also choose whether to convert automatically, not at all, or to 'ask'):

You will realise that we ended that demo just saving with no quantity selected. That's because we can use the same field in different template contexts to measure different quantities. So, next, we are going to add our field to a template and choose 'volume' as the quantity:

Similarly, we can add our field to a different template to measure mass:

Finally, we can create a page using one of these templates. In this case, it is 'volume' and we have chosen to convert automatically:

If we had chosen to 'ask', we would have got a confirmation box before doing the conversion.

All of this is accomplished by the magic of htmx (and of course ProcessWire). The principles behind it are discussed at 

The actual code has moved on a bit from that post. For instance, I have used css transitions in the config. These work really nicely with htmx:


#Inputfield_unit_set {
  opacity: 1;
  transition: opacity 200ms ease-in;
}

#Inputfield_unit_set.htmx-swapping {
  opacity: 0.1;
  transition: opacity 100ms ease-out;
}

#Inputfield_unit_set.htmx-settling {
  opacity: 0.1;
}

Now I'm getting the hang of htmx, I really like it ?

  • Like 4
Link to comment
Share on other sites

  • 2 weeks later...
  • 4 months later...
  • 3 weeks later...

version 0.0.19 adds 2 new Measurement methods: baseMultiplyBy() & baseDivideBy().

You can use these instead of multiplyBy() or divideBy(), where it is expected that the result of multiplyBy() or divideBy() would be an unknown quantity. These methods anticipate that the result will be a BaseMeasurement and avoids unnecessary warnings

Link to comment
Share on other sites

  • 1 year later...

Version 0.0.22 adds the ability to use the inputfield in module config fields (i.e. in a module's getModuleConfigInputfields method) but you need to 
create a measurement object and convert it to an array before it is saved. For example:

public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
	$modules = $this->wire()->modules;
.....

//Timeouts
    $m = $modules->get('FieldtypeMeasurement');
    $f= $modules->InputfieldMeasurement;
    $field = new Field();
    $field->setFieldtype($m);
    $field->set('quantity', 'Time');
    $field->set('units', ['second', 'minute', 'hour', 'day']);
    $f->setField($field);
    $f->setPage($this->page);
    $f_name = 'sessionTimeOut';
    $f->name = $f_name;
    $f->label = $this->_('Session Timeout');
    $f->description = $this->_('Set the session timeout period');
    $f->notes = $this->_('Max recommended = 1 day');
    $f->columnWidth = 50;

    // Get the posted values

    $postedMagnitude = $this->input->post["{$f_name}_magnitude"];
    $postedUnit = $this->input->post["{$f_name}_unit"];
    if($postedMagnitude && $postedUnit) {
    // Create a new Measurement object
    $measurement = $m->measurement("Time", $postedUnit, $postedMagnitude);
    // Save the posted value
    $f->value = $m->measurement_to_array($measurement);
    $this->$f_name = $f->value;

    } else {
    // Set the value of the input field to the saved value
    $f->value = $m->array_to_measurement($this->$f_name);
    }

    $inputfields->add($f);
    }

.....

}
 
  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...